@clubnet/seedclub 0.2.26 → 0.2.28
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.
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ The normal interactive flow is:
|
|
|
68
68
|
| `/login` | Sign in to a model provider for the underlying agent |
|
|
69
69
|
| `/model` | Choose which model to use |
|
|
70
70
|
| `/connect` | Connect your Seed Club account |
|
|
71
|
+
| `/connect-calendar` | Connect a personal Google Calendar to your Seed Club account |
|
|
71
72
|
| `/seedclub` | Main menu — connect, inspect access, and jump into CRM/meetings/media/headlines workflows |
|
|
72
73
|
| `/transcripts` | Export transcript VTT files with filters (date, person, time, output dir) |
|
|
73
74
|
|
|
@@ -81,6 +82,8 @@ There are two separate auth layers in the product:
|
|
|
81
82
|
This signs you into the LLM provider you want the agent to use.
|
|
82
83
|
2. Seed Club auth: `/connect`
|
|
83
84
|
This connects the CLI to your Seed Club account so Seed Club tools and commands can read and write account data.
|
|
85
|
+
3. Personal calendar connect: `/connect-calendar`
|
|
86
|
+
This connects a Google Calendar to your Seed Club account for booking and availability workflows. It is only needed if you want the agent to schedule using your personal calendar.
|
|
84
87
|
|
|
85
88
|
`/seedclub` is the main entry point for Seed Club actions. If you are not connected yet, it will start the Seed Club connect flow automatically.
|
|
86
89
|
|
|
@@ -16,6 +16,7 @@ import { getCurrentUser, getSessionContext } from "../tools/utility.js";
|
|
|
16
16
|
|
|
17
17
|
interface SeedclubDeps {
|
|
18
18
|
connect: (args: string | undefined, ctx: any) => Promise<void>;
|
|
19
|
+
connectCalendar: (ctx: any) => Promise<void>;
|
|
19
20
|
disconnect: (ctx: any) => Promise<void>;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -64,6 +65,13 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
64
65
|
},
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
pi.registerCommand("connect-calendar", {
|
|
69
|
+
description: "Connect a personal Google Calendar to your Seed Club account",
|
|
70
|
+
handler: async (_args, ctx) => {
|
|
71
|
+
await deps.connectCalendar(ctx);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
67
75
|
pi.registerCommand("clearcontext", {
|
|
68
76
|
description: "Compact the current conversation to free context while keeping Seed Club state",
|
|
69
77
|
handler: async (_args, ctx) => {
|
|
@@ -129,6 +137,7 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
129
137
|
"Show API/Auth environment",
|
|
130
138
|
"Use local API/Auth",
|
|
131
139
|
"Use prod API/Auth",
|
|
140
|
+
"Connect personal calendar",
|
|
132
141
|
"Open CRM prompt",
|
|
133
142
|
"Open meetings prompt",
|
|
134
143
|
"Open transcripts prompt",
|
|
@@ -168,6 +177,9 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
168
177
|
case "Use prod API/Auth":
|
|
169
178
|
await setSeedEnvironment("prod", ctx);
|
|
170
179
|
break;
|
|
180
|
+
case "Connect personal calendar":
|
|
181
|
+
await deps.connectCalendar(ctx);
|
|
182
|
+
break;
|
|
171
183
|
case "Open CRM prompt":
|
|
172
184
|
await new Promise((r) => setTimeout(r, 300));
|
|
173
185
|
ctx.ui.setEditorText("List CRM records for my workspace.");
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
9
9
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
10
10
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import { clearCredentials, setCachedToken } from "./api-client.js";
|
|
11
|
+
import { api, clearCredentials, setCachedToken } from "./api-client.js";
|
|
12
12
|
import { getApiBase, getAuthBase, getStoredBases, getStoredToken, storeToken } from "./auth.js";
|
|
13
13
|
import { registerSeedclubCommand } from "./commands/seedclub.js";
|
|
14
14
|
import { registerTranscriptsCommand } from "./commands/transcripts.js";
|
|
@@ -26,6 +26,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
26
26
|
return label;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
type PersonalCalendarAccount = {
|
|
30
|
+
display_name?: string | null;
|
|
31
|
+
external_account_id?: string | null;
|
|
32
|
+
id: string;
|
|
33
|
+
is_active?: boolean;
|
|
34
|
+
platform?: string | null;
|
|
35
|
+
username?: string | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type PersonalCalendarAccountsResponse = {
|
|
39
|
+
data?: PersonalCalendarAccount[];
|
|
40
|
+
};
|
|
41
|
+
|
|
29
42
|
// Tools
|
|
30
43
|
registerUtilityTools(pi);
|
|
31
44
|
registerCrmTools(pi);
|
|
@@ -36,7 +49,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
36
49
|
registerBrandingGuard(pi);
|
|
37
50
|
|
|
38
51
|
// Commands — /connect, /seedclub menu, and transcript export
|
|
39
|
-
registerSeedclubCommand(pi, { connect, disconnect });
|
|
52
|
+
registerSeedclubCommand(pi, { connect, connectCalendar, disconnect });
|
|
40
53
|
registerTranscriptsCommand(pi);
|
|
41
54
|
registerClipStatusCommand(pi);
|
|
42
55
|
// Transcript intent interceptor (metadata-first export flow).
|
|
@@ -93,10 +106,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
93
106
|
|
|
94
107
|
ctx.ui.notify("Opening browser to sign in...", "info");
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
pi.exec(openCmd, [authUrl]).catch(() => {
|
|
98
|
-
ctx.ui.notify(`Open this link:\n${authUrl}`, "info");
|
|
99
|
-
});
|
|
109
|
+
openExternalUrl(pi, authUrl, ctx);
|
|
100
110
|
|
|
101
111
|
try {
|
|
102
112
|
const result = await waitForCallback(port, state);
|
|
@@ -112,6 +122,76 @@ export default function (pi: ExtensionAPI) {
|
|
|
112
122
|
ctx.ui.notify("Logged out", "info");
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
async function listConnectedPersonalCalendars(ctx: any, options?: { notifyOnFailure?: boolean }) {
|
|
126
|
+
try {
|
|
127
|
+
const existing = await api.get<PersonalCalendarAccountsResponse>("/me/integrations/accounts", {
|
|
128
|
+
platform: "google_calendar",
|
|
129
|
+
});
|
|
130
|
+
return Array.isArray(existing.data) ? existing.data : [];
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (options?.notifyOnFailure !== false) {
|
|
133
|
+
const message = error instanceof Error ? error.message : "";
|
|
134
|
+
ctx.ui.notify(
|
|
135
|
+
message
|
|
136
|
+
? `Could not check existing calendar connection: ${message}`
|
|
137
|
+
: "Could not check existing calendar connection.",
|
|
138
|
+
"info",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function connectCalendar(ctx: any) {
|
|
146
|
+
const connectedCalendars = await listConnectedPersonalCalendars(ctx);
|
|
147
|
+
|
|
148
|
+
if (connectedCalendars && connectedCalendars.length > 0) {
|
|
149
|
+
const label =
|
|
150
|
+
connectedCalendars[0]?.display_name ||
|
|
151
|
+
connectedCalendars[0]?.username ||
|
|
152
|
+
connectedCalendars[0]?.external_account_id ||
|
|
153
|
+
"your Google Calendar";
|
|
154
|
+
ctx.ui.notify(`Personal calendar already connected: ${label}`, "info");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const port = await findAvailablePort();
|
|
159
|
+
const state = randomBytes(16).toString("hex");
|
|
160
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
161
|
+
|
|
162
|
+
type CalendarConnectResponse = {
|
|
163
|
+
authUrl: string;
|
|
164
|
+
callbackState?: string | null;
|
|
165
|
+
redirectUri: string;
|
|
166
|
+
returnTo?: string | null;
|
|
167
|
+
success: boolean;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let response: CalendarConnectResponse;
|
|
171
|
+
try {
|
|
172
|
+
response = await api.get<CalendarConnectResponse>("/me/integrations/google-calendar/connect", {
|
|
173
|
+
mode: "json",
|
|
174
|
+
callback_state: state,
|
|
175
|
+
callback_url: callbackUrl,
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const message = error instanceof Error ? error.message : "Unable to start calendar connect.";
|
|
179
|
+
ctx.ui.notify(message, "error");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ctx.ui.notify("Opening browser to connect Google Calendar...", "info");
|
|
184
|
+
openExternalUrl(pi, response.authUrl, ctx);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const result = await waitForCalendarCallback(port, state);
|
|
188
|
+
const label = result.accountLabel || result.accountUsername || "Google Calendar";
|
|
189
|
+
ctx.ui.notify(`Connected ${label}`, "info");
|
|
190
|
+
} catch (error) {
|
|
191
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Calendar connect failed", "error");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
115
195
|
async function verifyAndStore(token: string, ctx: any, emailHint?: string) {
|
|
116
196
|
const apiBase = getApiBase();
|
|
117
197
|
const authBase = getAuthBase();
|
|
@@ -221,6 +301,89 @@ function waitForCallback(port: number, state: string): Promise<{ token: string;
|
|
|
221
301
|
});
|
|
222
302
|
}
|
|
223
303
|
|
|
304
|
+
function openExternalUrl(pi: ExtensionAPI, url: string, ctx: any) {
|
|
305
|
+
const openCmd =
|
|
306
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
307
|
+
|
|
308
|
+
pi.exec(openCmd, [url]).catch(() => {
|
|
309
|
+
ctx.ui.notify(`Open this link:\n${url}`, "info");
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function waitForCalendarCallback(
|
|
314
|
+
port: number,
|
|
315
|
+
state: string,
|
|
316
|
+
): Promise<{ accountId: string | null; accountLabel: string | null; accountUsername: string | null }> {
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const timeout = setTimeout(() => {
|
|
319
|
+
server.close();
|
|
320
|
+
reject(new Error("Timed out waiting for Google Calendar connection (5 min)."));
|
|
321
|
+
}, 300_000);
|
|
322
|
+
|
|
323
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
324
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
325
|
+
if (url.pathname !== "/callback") {
|
|
326
|
+
res.writeHead(404);
|
|
327
|
+
res.end();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const done = (status: number, body: string) => {
|
|
332
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
333
|
+
res.end(body);
|
|
334
|
+
clearTimeout(timeout);
|
|
335
|
+
server.close();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (url.searchParams.get("state") !== state) {
|
|
339
|
+
done(400, renderCallbackPage({
|
|
340
|
+
eyebrow: "Google Calendar",
|
|
341
|
+
title: "Connection couldn’t be verified.",
|
|
342
|
+
message: "The calendar callback state did not match this session. Return to Seed Club and try /connect-calendar again.",
|
|
343
|
+
status: "error",
|
|
344
|
+
}));
|
|
345
|
+
reject(new Error("Invalid calendar callback state."));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const status = url.searchParams.get("status");
|
|
350
|
+
const error = url.searchParams.get("error");
|
|
351
|
+
|
|
352
|
+
if (status !== "success") {
|
|
353
|
+
done(400, renderCallbackPage({
|
|
354
|
+
eyebrow: "Google Calendar",
|
|
355
|
+
title: "Calendar connection failed.",
|
|
356
|
+
message: error || "Seed Club could not finish connecting your Google Calendar.",
|
|
357
|
+
status: "error",
|
|
358
|
+
}));
|
|
359
|
+
reject(new Error(error || "Google Calendar connection failed."));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const accountLabel = url.searchParams.get("accountLabel");
|
|
364
|
+
const accountUsername = url.searchParams.get("accountUsername");
|
|
365
|
+
|
|
366
|
+
done(200, renderCallbackPage({
|
|
367
|
+
eyebrow: "Google Calendar",
|
|
368
|
+
title: "Your calendar is connected.",
|
|
369
|
+
message: `Connected ${escapeHtml(accountLabel || accountUsername || "your Google Calendar")} to your Seed Club account.`,
|
|
370
|
+
status: "success",
|
|
371
|
+
}));
|
|
372
|
+
resolve({
|
|
373
|
+
accountId: url.searchParams.get("accountId"),
|
|
374
|
+
accountLabel,
|
|
375
|
+
accountUsername,
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
server.listen(port, "127.0.0.1");
|
|
380
|
+
server.on("error", (err) => {
|
|
381
|
+
clearTimeout(timeout);
|
|
382
|
+
reject(err);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
224
387
|
function escapeHtml(value: string): string {
|
|
225
388
|
return value
|
|
226
389
|
.replaceAll("&", "&")
|
|
@@ -11,6 +11,9 @@ const DEFAULT_SHOW_RECORDINGS_LIMIT = 10;
|
|
|
11
11
|
const MAX_SHOW_RECORDINGS_LIMIT = 25;
|
|
12
12
|
const DEFAULT_TRANSCRIPT_LIMIT = 5;
|
|
13
13
|
const MAX_TRANSCRIPT_LIMIT = 20;
|
|
14
|
+
const DEFAULT_PEOPLE_SEARCH_LIMIT = 5;
|
|
15
|
+
const MAX_PEOPLE_SEARCH_LIMIT = 10;
|
|
16
|
+
const DEFAULT_BOOKING_DURATION_MINUTES = 20;
|
|
14
17
|
const TRANSCRIPT_TEXT_PREVIEW_CHARS = 1800;
|
|
15
18
|
const TRANSCRIPT_VTT_PREVIEW_CHARS = 1000;
|
|
16
19
|
|
|
@@ -53,6 +56,103 @@ function truncateText(value: string | null | undefined, maxChars: number) {
|
|
|
53
56
|
return `${value.slice(0, maxChars)}\n...[truncated ${value.length - maxChars} chars]`;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
function resolveBookingEndsAt(
|
|
60
|
+
startsAt: string,
|
|
61
|
+
endsAt: string | null | undefined,
|
|
62
|
+
durationMinutes: number | null | undefined,
|
|
63
|
+
) {
|
|
64
|
+
if (typeof endsAt === "string" && endsAt.trim()) {
|
|
65
|
+
return endsAt;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const startDate = new Date(startsAt);
|
|
69
|
+
if (Number.isNaN(startDate.getTime())) {
|
|
70
|
+
return endsAt ?? undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const normalizedDuration =
|
|
74
|
+
typeof durationMinutes === "number" && Number.isFinite(durationMinutes) && durationMinutes > 0
|
|
75
|
+
? Math.round(durationMinutes)
|
|
76
|
+
: DEFAULT_BOOKING_DURATION_MINUTES;
|
|
77
|
+
|
|
78
|
+
return new Date(startDate.getTime() + normalizedDuration * 60 * 1000).toISOString();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function localMinutesToUTC(date: Date, minutes: number, timeZone: string) {
|
|
82
|
+
const year = date.getFullYear();
|
|
83
|
+
const month = date.getMonth();
|
|
84
|
+
const day = date.getDate();
|
|
85
|
+
const hours = Math.floor(minutes / 60);
|
|
86
|
+
const mins = minutes % 60;
|
|
87
|
+
|
|
88
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
89
|
+
timeZone,
|
|
90
|
+
year: "numeric",
|
|
91
|
+
month: "2-digit",
|
|
92
|
+
day: "2-digit",
|
|
93
|
+
hour: "2-digit",
|
|
94
|
+
minute: "2-digit",
|
|
95
|
+
second: "2-digit",
|
|
96
|
+
hour12: false,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const referenceUtc = new Date(Date.UTC(year, month, day, 12, 0, 0));
|
|
100
|
+
const parts = formatter.formatToParts(referenceUtc);
|
|
101
|
+
const tzYear = Number.parseInt(parts.find((part) => part.type === "year")?.value ?? "", 10);
|
|
102
|
+
const tzMonth = Number.parseInt(parts.find((part) => part.type === "month")?.value ?? "", 10) - 1;
|
|
103
|
+
const tzDay = Number.parseInt(parts.find((part) => part.type === "day")?.value ?? "", 10);
|
|
104
|
+
const tzHour = Number.parseInt(parts.find((part) => part.type === "hour")?.value ?? "", 10);
|
|
105
|
+
const tzMinute = Number.parseInt(parts.find((part) => part.type === "minute")?.value ?? "", 10);
|
|
106
|
+
const tzSecond = Number.parseInt(parts.find((part) => part.type === "second")?.value ?? "", 10);
|
|
107
|
+
|
|
108
|
+
const tzMillis = Date.UTC(tzYear, tzMonth, tzDay, tzHour, tzMinute, tzSecond);
|
|
109
|
+
const offset = referenceUtc.getTime() - tzMillis;
|
|
110
|
+
const desiredUtc = Date.UTC(year, month, day, hours, mins, 0);
|
|
111
|
+
return new Date(desiredUtc + offset);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getDateDayOfWeek(date: Date, timeZone: string) {
|
|
115
|
+
const weekday = new Intl.DateTimeFormat("en-US", {
|
|
116
|
+
timeZone,
|
|
117
|
+
weekday: "short",
|
|
118
|
+
})
|
|
119
|
+
.formatToParts(date)
|
|
120
|
+
.find((part) => part.type === "weekday")?.value;
|
|
121
|
+
|
|
122
|
+
switch (weekday) {
|
|
123
|
+
case "Sun":
|
|
124
|
+
return 0;
|
|
125
|
+
case "Mon":
|
|
126
|
+
return 1;
|
|
127
|
+
case "Tue":
|
|
128
|
+
return 2;
|
|
129
|
+
case "Wed":
|
|
130
|
+
return 3;
|
|
131
|
+
case "Thu":
|
|
132
|
+
return 4;
|
|
133
|
+
case "Fri":
|
|
134
|
+
return 5;
|
|
135
|
+
case "Sat":
|
|
136
|
+
return 6;
|
|
137
|
+
default:
|
|
138
|
+
return date.getDay();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatSlotRange(startMinutes: number, endMinutes: number, timeZone: string) {
|
|
143
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
144
|
+
timeZone,
|
|
145
|
+
hour: "numeric",
|
|
146
|
+
minute: "2-digit",
|
|
147
|
+
hour12: true,
|
|
148
|
+
timeZoneName: "short",
|
|
149
|
+
});
|
|
150
|
+
const anchor = new Date(Date.UTC(2026, 0, 5, 12, 0, 0));
|
|
151
|
+
const start = localMinutesToUTC(anchor, startMinutes, timeZone);
|
|
152
|
+
const end = localMinutesToUTC(anchor, endMinutes, timeZone);
|
|
153
|
+
return `${formatter.format(start)} - ${formatter.format(end)}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
56
156
|
function shapeTranscriptSource(source: any, includeFullText: boolean, includePreview = true) {
|
|
57
157
|
if (!source) {
|
|
58
158
|
return null;
|
|
@@ -1024,6 +1124,345 @@ async function getMeeting(args: { meetingId: string }) {
|
|
|
1024
1124
|
}
|
|
1025
1125
|
}
|
|
1026
1126
|
|
|
1127
|
+
async function searchPeopleForBooking(args: {
|
|
1128
|
+
query: string;
|
|
1129
|
+
limit?: number;
|
|
1130
|
+
}) {
|
|
1131
|
+
try {
|
|
1132
|
+
const limit = normalizeLimit(args.limit, {
|
|
1133
|
+
fallback: DEFAULT_PEOPLE_SEARCH_LIMIT,
|
|
1134
|
+
max: MAX_PEOPLE_SEARCH_LIMIT,
|
|
1135
|
+
});
|
|
1136
|
+
const response = await api.get<any>("/core/parties", {
|
|
1137
|
+
party_type: "person",
|
|
1138
|
+
search: args.query,
|
|
1139
|
+
limit,
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
return {
|
|
1143
|
+
data: Array.isArray(response?.data)
|
|
1144
|
+
? response.data.map((item: any) => ({
|
|
1145
|
+
partyId: item?.party?.id ?? item?.party?.party_id ?? item?.person?.id ?? null,
|
|
1146
|
+
displayName:
|
|
1147
|
+
item?.person?.full_name ??
|
|
1148
|
+
item?.party?.display_name ??
|
|
1149
|
+
item?.organization?.name ??
|
|
1150
|
+
null,
|
|
1151
|
+
email: item?.person?.email ?? null,
|
|
1152
|
+
avatarUrl: item?.person?.avatar_url ?? null,
|
|
1153
|
+
twitterUsername: item?.person?.twitter_username ?? null,
|
|
1154
|
+
organizationName:
|
|
1155
|
+
item?.primary_org_role?.organization_name ??
|
|
1156
|
+
item?.organization?.name ??
|
|
1157
|
+
null,
|
|
1158
|
+
}))
|
|
1159
|
+
: [],
|
|
1160
|
+
pagination: response?.pagination ?? null,
|
|
1161
|
+
};
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function listMeetingAvailabilitySlots(args: {
|
|
1169
|
+
date: string;
|
|
1170
|
+
programSlug?: string;
|
|
1171
|
+
timeZone?: string;
|
|
1172
|
+
}) {
|
|
1173
|
+
try {
|
|
1174
|
+
const requestedDate = new Date(args.date);
|
|
1175
|
+
if (Number.isNaN(requestedDate.getTime())) {
|
|
1176
|
+
return { error: "date must be a valid ISO date or timestamp.", status: 400 };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const requestedTimeZone =
|
|
1180
|
+
typeof args.timeZone === "string" && args.timeZone.trim()
|
|
1181
|
+
? args.timeZone.trim()
|
|
1182
|
+
: "America/New_York";
|
|
1183
|
+
const dayOfWeek = getDateDayOfWeek(requestedDate, requestedTimeZone);
|
|
1184
|
+
const response = await api.get<any>("/meetings/availability", {
|
|
1185
|
+
program_slug: args.programSlug,
|
|
1186
|
+
day_of_week: dayOfWeek,
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
const slots = Array.isArray(response?.data)
|
|
1190
|
+
? response.data.map((window: any) => {
|
|
1191
|
+
const slotTimeZone =
|
|
1192
|
+
typeof window?.timezone === "string" && window.timezone.trim()
|
|
1193
|
+
? window.timezone.trim()
|
|
1194
|
+
: requestedTimeZone;
|
|
1195
|
+
const startsAt = localMinutesToUTC(requestedDate, Number(window?.start_minutes ?? 0), slotTimeZone);
|
|
1196
|
+
const endsAt = localMinutesToUTC(requestedDate, Number(window?.end_minutes ?? 0), slotTimeZone);
|
|
1197
|
+
|
|
1198
|
+
return {
|
|
1199
|
+
id: window?.id ?? null,
|
|
1200
|
+
label: formatSlotRange(
|
|
1201
|
+
Number(window?.start_minutes ?? 0),
|
|
1202
|
+
Number(window?.end_minutes ?? 0),
|
|
1203
|
+
slotTimeZone,
|
|
1204
|
+
),
|
|
1205
|
+
timeZone: slotTimeZone,
|
|
1206
|
+
startMinutes: window?.start_minutes ?? null,
|
|
1207
|
+
endMinutes: window?.end_minutes ?? null,
|
|
1208
|
+
startsAt: startsAt.toISOString(),
|
|
1209
|
+
endsAt: endsAt.toISOString(),
|
|
1210
|
+
displayRange: formatSlotRange(
|
|
1211
|
+
Number(window?.start_minutes ?? 0),
|
|
1212
|
+
Number(window?.end_minutes ?? 0),
|
|
1213
|
+
slotTimeZone,
|
|
1214
|
+
),
|
|
1215
|
+
};
|
|
1216
|
+
})
|
|
1217
|
+
: [];
|
|
1218
|
+
|
|
1219
|
+
return {
|
|
1220
|
+
date: args.date,
|
|
1221
|
+
dayOfWeek,
|
|
1222
|
+
slots,
|
|
1223
|
+
};
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1226
|
+
throw error;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
async function listMeetingCalendars(args: {
|
|
1231
|
+
programSlug?: string;
|
|
1232
|
+
}) {
|
|
1233
|
+
try {
|
|
1234
|
+
return await api.get<any>("/meetings/calendars", {
|
|
1235
|
+
program_slug: args.programSlug,
|
|
1236
|
+
});
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1239
|
+
throw error;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async function resolvePersonalCalendarAccountId(accountId?: string | null) {
|
|
1244
|
+
const response = await api.get<any>("/me/integrations/accounts", {
|
|
1245
|
+
platform: "google_calendar",
|
|
1246
|
+
});
|
|
1247
|
+
const accounts = Array.isArray(response?.data) ? response.data : [];
|
|
1248
|
+
const activeAccounts = accounts.filter((account: any) => account?.is_active !== false);
|
|
1249
|
+
|
|
1250
|
+
if (!activeAccounts.length) {
|
|
1251
|
+
throw new Error("No personal Google Calendar is connected. Run /connect-calendar first.");
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const normalizedIdentifier = typeof accountId === "string" ? accountId.trim().toLowerCase() : "";
|
|
1255
|
+
|
|
1256
|
+
if (!normalizedIdentifier) {
|
|
1257
|
+
if (activeAccounts.length === 1) {
|
|
1258
|
+
return String(activeAccounts[0]?.id);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const choices = activeAccounts
|
|
1262
|
+
.map((account: any) => {
|
|
1263
|
+
const label = account?.display_name || account?.external_account_id || account?.username || account?.id;
|
|
1264
|
+
const externalAccountId = typeof account?.external_account_id === "string" ? account.external_account_id : null;
|
|
1265
|
+
return externalAccountId && externalAccountId !== label ? `${label} (${externalAccountId})` : String(label);
|
|
1266
|
+
})
|
|
1267
|
+
.join(", ");
|
|
1268
|
+
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
`Multiple personal Google Calendars are connected. Pass accountId as the calendar id, external_account_id, or display name. Available calendars: ${choices}`,
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const matches = activeAccounts.filter((account: any) => {
|
|
1275
|
+
const candidates = [
|
|
1276
|
+
typeof account?.id === "string" ? account.id : null,
|
|
1277
|
+
typeof account?.external_account_id === "string" ? account.external_account_id : null,
|
|
1278
|
+
typeof account?.display_name === "string" ? account.display_name : null,
|
|
1279
|
+
typeof account?.username === "string" ? account.username : null,
|
|
1280
|
+
];
|
|
1281
|
+
|
|
1282
|
+
return candidates.some((candidate) => candidate?.trim().toLowerCase() === normalizedIdentifier);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (!matches.length) {
|
|
1286
|
+
throw new Error(
|
|
1287
|
+
`No connected personal Google Calendar matched "${accountId}". Pass the calendar id, external_account_id, or display name from /me/integrations/accounts.`,
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (matches.length > 1) {
|
|
1292
|
+
const choices = matches
|
|
1293
|
+
.map((account: any) => {
|
|
1294
|
+
const label = account?.display_name || account?.external_account_id || account?.username || account?.id;
|
|
1295
|
+
const externalAccountId = typeof account?.external_account_id === "string" ? account.external_account_id : null;
|
|
1296
|
+
return externalAccountId && externalAccountId !== label ? `${label} (${externalAccountId})` : String(label);
|
|
1297
|
+
})
|
|
1298
|
+
.join(", ");
|
|
1299
|
+
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`"${accountId}" matched multiple personal Google Calendars. Pass a unique calendar id or external_account_id. Matches: ${choices}`,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return String(matches[0]?.id);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async function listPersonalCalendarEvents(args: {
|
|
1309
|
+
accountId?: string | null;
|
|
1310
|
+
timeMin?: string | null;
|
|
1311
|
+
timeMax?: string | null;
|
|
1312
|
+
timeZone?: string | null;
|
|
1313
|
+
maxResults?: number | null;
|
|
1314
|
+
query?: string | null;
|
|
1315
|
+
}) {
|
|
1316
|
+
try {
|
|
1317
|
+
const accountId = await resolvePersonalCalendarAccountId(args.accountId);
|
|
1318
|
+
return await api.get<any>(`/me/integrations/accounts/${accountId}/events`, {
|
|
1319
|
+
max_results: args.maxResults ?? undefined,
|
|
1320
|
+
q: args.query ?? undefined,
|
|
1321
|
+
time_max: args.timeMax ?? undefined,
|
|
1322
|
+
time_min: args.timeMin ?? undefined,
|
|
1323
|
+
time_zone: args.timeZone ?? undefined,
|
|
1324
|
+
});
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1327
|
+
throw error;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function createPersonalCalendarEvent(args: {
|
|
1332
|
+
accountId?: string | null;
|
|
1333
|
+
summary: string;
|
|
1334
|
+
startTime: string;
|
|
1335
|
+
endTime: string;
|
|
1336
|
+
description?: string | null;
|
|
1337
|
+
timeZone?: string | null;
|
|
1338
|
+
attendeeEmails?: string[] | null;
|
|
1339
|
+
}) {
|
|
1340
|
+
try {
|
|
1341
|
+
const accountId = await resolvePersonalCalendarAccountId(args.accountId);
|
|
1342
|
+
return await api.post<any>(`/me/integrations/accounts/${accountId}/events`, {
|
|
1343
|
+
attendee_emails: Array.isArray(args.attendeeEmails) ? args.attendeeEmails : undefined,
|
|
1344
|
+
description: args.description ?? undefined,
|
|
1345
|
+
end_time: args.endTime,
|
|
1346
|
+
start_time: args.startTime,
|
|
1347
|
+
summary: args.summary,
|
|
1348
|
+
time_zone: args.timeZone ?? undefined,
|
|
1349
|
+
});
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
async function updatePersonalCalendarEvent(args: {
|
|
1357
|
+
accountId?: string | null;
|
|
1358
|
+
eventId: string;
|
|
1359
|
+
summary?: string | null;
|
|
1360
|
+
startTime?: string | null;
|
|
1361
|
+
endTime?: string | null;
|
|
1362
|
+
description?: string | null;
|
|
1363
|
+
timeZone?: string | null;
|
|
1364
|
+
attendeeEmails?: string[] | null;
|
|
1365
|
+
}) {
|
|
1366
|
+
try {
|
|
1367
|
+
const accountId = await resolvePersonalCalendarAccountId(args.accountId);
|
|
1368
|
+
return await api.patch<any>(`/me/integrations/accounts/${accountId}/events/${args.eventId}`, {
|
|
1369
|
+
attendee_emails: Array.isArray(args.attendeeEmails) ? args.attendeeEmails : undefined,
|
|
1370
|
+
description: args.description ?? undefined,
|
|
1371
|
+
end_time: args.endTime ?? undefined,
|
|
1372
|
+
start_time: args.startTime ?? undefined,
|
|
1373
|
+
summary: args.summary ?? undefined,
|
|
1374
|
+
time_zone: args.timeZone ?? undefined,
|
|
1375
|
+
});
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function deletePersonalCalendarEvent(args: {
|
|
1383
|
+
accountId?: string | null;
|
|
1384
|
+
eventId: string;
|
|
1385
|
+
}) {
|
|
1386
|
+
try {
|
|
1387
|
+
const accountId = await resolvePersonalCalendarAccountId(args.accountId);
|
|
1388
|
+
return await api.delete<any>(`/me/integrations/accounts/${accountId}/events/${args.eventId}`);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1391
|
+
throw error;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
async function findCommonMeetingAvailability(args: {
|
|
1396
|
+
calendarAccountIds: string[];
|
|
1397
|
+
from: string;
|
|
1398
|
+
to: string;
|
|
1399
|
+
timeZone?: string;
|
|
1400
|
+
slotDurationMinutes: number;
|
|
1401
|
+
candidateSlots?: Array<{
|
|
1402
|
+
startsAt: string;
|
|
1403
|
+
endsAt: string;
|
|
1404
|
+
}> | null;
|
|
1405
|
+
}) {
|
|
1406
|
+
try {
|
|
1407
|
+
return await api.post<any>("/meetings/calendar-availability", {
|
|
1408
|
+
calendar_account_ids: args.calendarAccountIds,
|
|
1409
|
+
from: args.from,
|
|
1410
|
+
to: args.to,
|
|
1411
|
+
time_zone: args.timeZone,
|
|
1412
|
+
slot_duration_minutes: args.slotDurationMinutes,
|
|
1413
|
+
candidate_slots: Array.isArray(args.candidateSlots)
|
|
1414
|
+
? args.candidateSlots.map((slot) => ({
|
|
1415
|
+
starts_at: slot.startsAt,
|
|
1416
|
+
ends_at: slot.endsAt,
|
|
1417
|
+
}))
|
|
1418
|
+
: undefined,
|
|
1419
|
+
});
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
async function bookMeetingFull(args: {
|
|
1427
|
+
startsAt: string;
|
|
1428
|
+
endsAt?: string | null;
|
|
1429
|
+
durationMinutes?: number | null;
|
|
1430
|
+
guestName: string;
|
|
1431
|
+
guestEmail: string;
|
|
1432
|
+
timeZone?: string;
|
|
1433
|
+
title?: string | null;
|
|
1434
|
+
description?: string | null;
|
|
1435
|
+
partyId?: string | null;
|
|
1436
|
+
guestTwitterHandle?: string | null;
|
|
1437
|
+
profileImageUrl?: string | null;
|
|
1438
|
+
programSlug?: string;
|
|
1439
|
+
calendarAccountId?: string | null;
|
|
1440
|
+
}) {
|
|
1441
|
+
try {
|
|
1442
|
+
const resolvedEndsAt = resolveBookingEndsAt(args.startsAt, args.endsAt, args.durationMinutes);
|
|
1443
|
+
return await api.post<any>("/meetings/book-full", {
|
|
1444
|
+
calendar_account_id: args.calendarAccountId,
|
|
1445
|
+
program_slug: args.programSlug,
|
|
1446
|
+
starts_at: args.startsAt,
|
|
1447
|
+
ends_at: resolvedEndsAt,
|
|
1448
|
+
duration_minutes: args.durationMinutes,
|
|
1449
|
+
time_zone: args.timeZone,
|
|
1450
|
+
title: args.title,
|
|
1451
|
+
description: args.description,
|
|
1452
|
+
guest: {
|
|
1453
|
+
party_id: args.partyId,
|
|
1454
|
+
full_name: args.guestName,
|
|
1455
|
+
email: args.guestEmail,
|
|
1456
|
+
twitter_handle: args.guestTwitterHandle,
|
|
1457
|
+
profile_image_url: args.profileImageUrl,
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
1462
|
+
throw error;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1027
1466
|
async function updateMeeting(args: {
|
|
1028
1467
|
meetingId: string;
|
|
1029
1468
|
startsAt?: string | null;
|
|
@@ -1061,6 +1500,136 @@ async function assignMeeting(args: {
|
|
|
1061
1500
|
}
|
|
1062
1501
|
|
|
1063
1502
|
export function registerMeetingTools(pi: ExtensionAPI) {
|
|
1503
|
+
pi.registerTool({
|
|
1504
|
+
name: "seedclub_search_people",
|
|
1505
|
+
label: "Search People",
|
|
1506
|
+
description:
|
|
1507
|
+
"Search people in Seed Club before booking a meeting. Use this first to find an existing guest and get a partyId. If multiple plausible matches appear, ask the user to choose before booking.",
|
|
1508
|
+
parameters: Type.Object({
|
|
1509
|
+
query: Type.String({ description: "Guest name, email, or search text" }),
|
|
1510
|
+
limit: Type.Optional(
|
|
1511
|
+
Type.Number({ description: "Maximum matches to return (defaults to 5, max 10)." }),
|
|
1512
|
+
),
|
|
1513
|
+
}),
|
|
1514
|
+
execute: wrapExecute(searchPeopleForBooking),
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
pi.registerTool({
|
|
1518
|
+
name: "seedclub_list_meeting_availability",
|
|
1519
|
+
label: "List Availability Slots",
|
|
1520
|
+
description:
|
|
1521
|
+
"List the configured availability slots for a specific date. Use this before booking and choose one of the returned slot startsAt/endsAt pairs instead of inventing a meeting length or time.",
|
|
1522
|
+
parameters: Type.Object({
|
|
1523
|
+
date: Type.String({
|
|
1524
|
+
description: "Target date as YYYY-MM-DD or ISO timestamp. The tool returns valid startsAt/endsAt pairs for that day.",
|
|
1525
|
+
}),
|
|
1526
|
+
programSlug: Type.Optional(Type.String({ description: "Program slug. Defaults to 11am on the server." })),
|
|
1527
|
+
timeZone: Type.Optional(Type.String({ description: "Optional timezone used to interpret the requested date. Defaults to America/New_York." })),
|
|
1528
|
+
}),
|
|
1529
|
+
execute: wrapExecute(listMeetingAvailabilitySlots),
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
pi.registerTool({
|
|
1533
|
+
name: "seedclub_list_meeting_calendars",
|
|
1534
|
+
label: "List Booking Calendars",
|
|
1535
|
+
description:
|
|
1536
|
+
"List the connected Google calendars that can be used for meeting scheduling. Returns both shared 11am calendars and personal calendars that the current user can book on.",
|
|
1537
|
+
parameters: Type.Object({
|
|
1538
|
+
programSlug: Type.Optional(Type.String({ description: "Program slug. Defaults to 11am on the server." })),
|
|
1539
|
+
}),
|
|
1540
|
+
execute: wrapExecute(listMeetingCalendars),
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
pi.registerTool({
|
|
1544
|
+
name: "seedclub_list_personal_calendar_events",
|
|
1545
|
+
label: "List Personal Calendar Events",
|
|
1546
|
+
description:
|
|
1547
|
+
"List events from a connected personal Google Calendar. Use this for personal calendar read access. accountId may be the calendar id, external_account_id, or display name. If omitted, the tool only auto-selects when exactly one active personal calendar is connected.",
|
|
1548
|
+
parameters: Type.Object({
|
|
1549
|
+
accountId: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional calendar identifier: account id, external_account_id (email), or display name." })),
|
|
1550
|
+
timeMin: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional ISO range start." })),
|
|
1551
|
+
timeMax: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional ISO range end." })),
|
|
1552
|
+
timeZone: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional IANA timezone." })),
|
|
1553
|
+
maxResults: Type.Optional(Type.Union([Type.Number(), Type.Null()], { description: "Optional max results, defaults to 25 on the server." })),
|
|
1554
|
+
query: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional Google Calendar search query." })),
|
|
1555
|
+
}),
|
|
1556
|
+
execute: wrapExecute(listPersonalCalendarEvents),
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
pi.registerTool({
|
|
1560
|
+
name: "seedclub_create_personal_calendar_event",
|
|
1561
|
+
label: "Create Personal Calendar Event",
|
|
1562
|
+
description:
|
|
1563
|
+
"Create an event on a connected personal Google Calendar. accountId may be the calendar id, external_account_id, or display name. If omitted, the tool only auto-selects when exactly one active personal calendar is connected.",
|
|
1564
|
+
parameters: Type.Object({
|
|
1565
|
+
accountId: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional calendar identifier: account id, external_account_id (email), or display name." })),
|
|
1566
|
+
summary: Type.String({ description: "Event title/summary." }),
|
|
1567
|
+
startTime: Type.String({ description: "Event start time as an ISO timestamp." }),
|
|
1568
|
+
endTime: Type.String({ description: "Event end time as an ISO timestamp." }),
|
|
1569
|
+
description: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1570
|
+
timeZone: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1571
|
+
attendeeEmails: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
|
|
1572
|
+
}),
|
|
1573
|
+
execute: wrapExecute(createPersonalCalendarEvent),
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
pi.registerTool({
|
|
1577
|
+
name: "seedclub_update_personal_calendar_event",
|
|
1578
|
+
label: "Update Personal Calendar Event",
|
|
1579
|
+
description:
|
|
1580
|
+
"Update an event on a connected personal Google Calendar. accountId may be the calendar id, external_account_id, or display name. If omitted, the tool only auto-selects when exactly one active personal calendar is connected.",
|
|
1581
|
+
parameters: Type.Object({
|
|
1582
|
+
accountId: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional calendar identifier: account id, external_account_id (email), or display name." })),
|
|
1583
|
+
eventId: Type.String({ description: "Google Calendar event id." }),
|
|
1584
|
+
summary: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1585
|
+
startTime: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1586
|
+
endTime: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1587
|
+
description: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1588
|
+
timeZone: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1589
|
+
attendeeEmails: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
|
|
1590
|
+
}),
|
|
1591
|
+
execute: wrapExecute(updatePersonalCalendarEvent),
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
pi.registerTool({
|
|
1595
|
+
name: "seedclub_delete_personal_calendar_event",
|
|
1596
|
+
label: "Delete Personal Calendar Event",
|
|
1597
|
+
description:
|
|
1598
|
+
"Delete an event from a connected personal Google Calendar. accountId may be the calendar id, external_account_id, or display name. If omitted, the tool only auto-selects when exactly one active personal calendar is connected.",
|
|
1599
|
+
parameters: Type.Object({
|
|
1600
|
+
accountId: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Optional calendar identifier: account id, external_account_id (email), or display name." })),
|
|
1601
|
+
eventId: Type.String({ description: "Google Calendar event id." }),
|
|
1602
|
+
}),
|
|
1603
|
+
execute: wrapExecute(deletePersonalCalendarEvent),
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
pi.registerTool({
|
|
1607
|
+
name: "seedclub_find_common_meeting_availability",
|
|
1608
|
+
label: "Find Common Availability",
|
|
1609
|
+
description:
|
|
1610
|
+
"Check calendar overlap across multiple connected calendars. Use this for common-meeting scheduling. You can pass candidateSlots from seedclub_list_meeting_availability to filter down to allowed booking slots, then book on one chosen calendar.",
|
|
1611
|
+
parameters: Type.Object({
|
|
1612
|
+
calendarAccountIds: Type.Array(
|
|
1613
|
+
Type.String({ description: "Calendar account ids to compare for overlap." }),
|
|
1614
|
+
),
|
|
1615
|
+
from: Type.String({ description: "Range start as an ISO timestamp." }),
|
|
1616
|
+
to: Type.String({ description: "Range end as an ISO timestamp." }),
|
|
1617
|
+
timeZone: Type.Optional(Type.String({ description: "IANA timezone like America/New_York." })),
|
|
1618
|
+
slotDurationMinutes: Type.Number({
|
|
1619
|
+
description: "Desired meeting length in minutes.",
|
|
1620
|
+
}),
|
|
1621
|
+
candidateSlots: Type.Optional(
|
|
1622
|
+
Type.Array(
|
|
1623
|
+
Type.Object({
|
|
1624
|
+
startsAt: Type.String({ description: "Candidate slot start as an ISO timestamp." }),
|
|
1625
|
+
endsAt: Type.String({ description: "Candidate slot end as an ISO timestamp." }),
|
|
1626
|
+
}),
|
|
1627
|
+
),
|
|
1628
|
+
),
|
|
1629
|
+
}),
|
|
1630
|
+
execute: wrapExecute(findCommonMeetingAvailability),
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1064
1633
|
pi.registerTool({
|
|
1065
1634
|
name: "seedclub_list_meetings",
|
|
1066
1635
|
label: "List Meetings",
|
|
@@ -1358,6 +1927,41 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1358
1927
|
renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_get_meeting"), (details) => details?.meeting?.id || undefined),
|
|
1359
1928
|
});
|
|
1360
1929
|
|
|
1930
|
+
pi.registerTool({
|
|
1931
|
+
name: "seedclub_book_meeting_full",
|
|
1932
|
+
label: "Book Meeting",
|
|
1933
|
+
description:
|
|
1934
|
+
"Book one guest meeting end-to-end: create the meeting, create the studio link, and create the Google Calendar invite. Always fetch availability first with seedclub_list_meeting_availability and use one of the returned startsAt/endsAt pairs. For common meetings, use seedclub_list_meeting_calendars and seedclub_find_common_meeting_availability first, then pass the chosen calendarAccountId. Only use this after you know guestName, guestEmail, and startsAt. Prefer passing endsAt when using an availability slot. If they only give a slot or duration, pass durationMinutes instead; if neither is available, this tool defaults to a 20-minute meeting, but the backend will still reject times that do not exactly match a configured slot. Search for an existing guest first and pass partyId when you have a confident match; otherwise proceed without partyId and the backend will find or create a contact using guestEmail before booking.",
|
|
1935
|
+
parameters: Type.Object({
|
|
1936
|
+
startsAt: Type.String({ description: "Meeting start time as an ISO timestamp" }),
|
|
1937
|
+
endsAt: Type.Optional(
|
|
1938
|
+
Type.Union([Type.String(), Type.Null()], {
|
|
1939
|
+
description: "Optional meeting end time as an ISO timestamp. If omitted, durationMinutes or the default 20-minute duration is used.",
|
|
1940
|
+
}),
|
|
1941
|
+
),
|
|
1942
|
+
durationMinutes: Type.Optional(
|
|
1943
|
+
Type.Union([Type.Number(), Type.Null()], {
|
|
1944
|
+
description: "Optional meeting length in minutes. Use this when the user gives a duration or a 20-minute slot instead of an explicit end time.",
|
|
1945
|
+
}),
|
|
1946
|
+
),
|
|
1947
|
+
guestName: Type.String({ description: "Guest full name" }),
|
|
1948
|
+
guestEmail: Type.String({ description: "Guest email address" }),
|
|
1949
|
+
timeZone: Type.Optional(Type.String({ description: "IANA timezone like America/New_York" })),
|
|
1950
|
+
title: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1951
|
+
description: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1952
|
+
partyId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1953
|
+
calendarAccountId: Type.Optional(
|
|
1954
|
+
Type.Union([Type.String(), Type.Null()], {
|
|
1955
|
+
description: "Optional calendar account id to write the event on. Use this for common meetings or when multiple writable calendars exist.",
|
|
1956
|
+
}),
|
|
1957
|
+
),
|
|
1958
|
+
guestTwitterHandle: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1959
|
+
profileImageUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1960
|
+
programSlug: Type.Optional(Type.String({ description: "Program slug. Defaults to 11am on the server." })),
|
|
1961
|
+
}),
|
|
1962
|
+
execute: wrapExecute(bookMeetingFull),
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1361
1965
|
pi.registerTool({
|
|
1362
1966
|
name: "seedclub_update_meeting",
|
|
1363
1967
|
label: "Update Meeting",
|
package/package.json
CHANGED