@clubnet/seedclub 0.2.27 → 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
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
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("&", "&amp;")
@@ -1227,6 +1227,202 @@ async function listMeetingAvailabilitySlots(args: {
1227
1227
  }
1228
1228
  }
1229
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
+
1230
1426
  async function bookMeetingFull(args: {
1231
1427
  startsAt: string;
1232
1428
  endsAt?: string | null;
@@ -1240,10 +1436,12 @@ async function bookMeetingFull(args: {
1240
1436
  guestTwitterHandle?: string | null;
1241
1437
  profileImageUrl?: string | null;
1242
1438
  programSlug?: string;
1439
+ calendarAccountId?: string | null;
1243
1440
  }) {
1244
1441
  try {
1245
1442
  const resolvedEndsAt = resolveBookingEndsAt(args.startsAt, args.endsAt, args.durationMinutes);
1246
1443
  return await api.post<any>("/meetings/book-full", {
1444
+ calendar_account_id: args.calendarAccountId,
1247
1445
  program_slug: args.programSlug,
1248
1446
  starts_at: args.startsAt,
1249
1447
  ends_at: resolvedEndsAt,
@@ -1331,6 +1529,107 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1331
1529
  execute: wrapExecute(listMeetingAvailabilitySlots),
1332
1530
  });
1333
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
+
1334
1633
  pi.registerTool({
1335
1634
  name: "seedclub_list_meetings",
1336
1635
  label: "List Meetings",
@@ -1632,7 +1931,7 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1632
1931
  name: "seedclub_book_meeting_full",
1633
1932
  label: "Book Meeting",
1634
1933
  description:
1635
- "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. 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.",
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.",
1636
1935
  parameters: Type.Object({
1637
1936
  startsAt: Type.String({ description: "Meeting start time as an ISO timestamp" }),
1638
1937
  endsAt: Type.Optional(
@@ -1651,6 +1950,11 @@ export function registerMeetingTools(pi: ExtensionAPI) {
1651
1950
  title: Type.Optional(Type.Union([Type.String(), Type.Null()])),
1652
1951
  description: Type.Optional(Type.Union([Type.String(), Type.Null()])),
1653
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
+ ),
1654
1958
  guestTwitterHandle: Type.Optional(Type.Union([Type.String(), Type.Null()])),
1655
1959
  profileImageUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
1656
1960
  programSlug: Type.Optional(Type.String({ description: "Program slug. Defaults to 11am on the server." })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubnet/seedclub",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "A branded command-line agent wrapper around pi, with integrated Seed Club commands, tools, and app actions",
5
5
  "license": "MIT",
6
6
  "repository": {