@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
|
-
|
|
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("&", "&")
|
|
@@ -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