@hmawla/co-assistant 1.0.6 → 1.0.8

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "gmail",
3
3
  "name": "Gmail Plugin",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "description": "Send, read, and search Gmail messages via the Gmail API",
6
6
  "author": "co-assistant",
7
7
  "requiredCredentials": [
@@ -117,7 +117,7 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
117
117
  const searchEmails: ToolDefinition = {
118
118
  name: "search_emails",
119
119
  description:
120
- "Search for emails in Gmail using a query string (same syntax as the Gmail search bar)",
120
+ "Search for emails in Gmail. Returns metadata by default; set includeBody=true to also return full message bodies (avoids needing separate read_email calls).",
121
121
  parameters: z.object({
122
122
  /** Gmail search query (e.g. "from:alice subject:meeting"). */
123
123
  query: z.string().describe("Gmail search query"),
@@ -130,13 +130,21 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
130
130
  .optional()
131
131
  .default(10)
132
132
  .describe("Maximum number of results to return"),
133
+ /** When true, fetches full message bodies inline. Slower per message but
134
+ * eliminates the need for separate read_email calls. */
135
+ includeBody: z
136
+ .boolean()
137
+ .optional()
138
+ .default(false)
139
+ .describe("Include full message body in results"),
133
140
  }),
134
141
 
135
142
  handler: async (args) => {
136
143
  try {
137
144
  const query = args.query as string;
138
145
  const maxResults = (args.maxResults as number | undefined) ?? 10;
139
- logger.debug({ query, maxResults }, "search_emails called");
146
+ const includeBody = (args.includeBody as boolean | undefined) ?? false;
147
+ logger.debug({ query, maxResults, includeBody }, "search_emails called");
140
148
 
141
149
  // Step 1 — List message IDs matching the query.
142
150
  const params = new URLSearchParams({
@@ -162,32 +170,47 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
162
170
  return "No emails found matching that query.";
163
171
  }
164
172
 
165
- // Step 2 — Fetch each message's metadata (Subject, From, Date, snippet).
173
+ // Step 2 — Fetch each message. Use "full" format when body is
174
+ // requested, otherwise "metadata" for a lighter response.
175
+ const format = includeBody ? "full" : "metadata";
166
176
  const headers = await authHeaders(auth);
167
177
  const results = await Promise.all(
168
178
  listData.messages.map(async (msg) => {
169
- const msgRes = await fetch(
170
- `${GMAIL_API}/messages/${msg.id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`,
171
- { headers },
172
- );
179
+ const url = includeBody
180
+ ? `${GMAIL_API}/messages/${msg.id}?format=${format}`
181
+ : `${GMAIL_API}/messages/${msg.id}?format=${format}&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`;
182
+ const msgRes = await fetch(url, { headers });
173
183
 
174
184
  if (!msgRes.ok) {
175
- return { id: msg.id, error: `Failed to fetch (${msgRes.status})` };
185
+ return { id: msg.id, threadId: msg.threadId, error: `Failed to fetch (${msgRes.status})` };
176
186
  }
177
187
 
178
188
  const msgData = (await msgRes.json()) as {
179
189
  id: string;
190
+ threadId: string;
180
191
  snippet: string;
181
- payload: { headers: Array<{ name: string; value: string }> };
192
+ payload: {
193
+ headers: Array<{ name: string; value: string }>;
194
+ body?: { data?: string };
195
+ parts?: Array<Record<string, unknown>>;
196
+ mimeType?: string;
197
+ };
182
198
  };
183
199
 
184
- return {
200
+ const result: Record<string, unknown> = {
185
201
  id: msgData.id,
202
+ threadId: msgData.threadId,
186
203
  subject: getHeader(msgData.payload.headers, "Subject"),
187
204
  from: getHeader(msgData.payload.headers, "From"),
188
205
  date: getHeader(msgData.payload.headers, "Date"),
189
206
  snippet: msgData.snippet,
190
207
  };
208
+
209
+ if (includeBody) {
210
+ result.body = extractBody(msgData.payload as Record<string, unknown>);
211
+ }
212
+
213
+ return result;
191
214
  }),
192
215
  );
193
216
 
@@ -332,5 +355,211 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
332
355
  },
333
356
  };
334
357
 
335
- return [searchEmails, readEmail, sendEmail];
358
+ // -----------------------------------------------------------------------
359
+ // get_thread
360
+ // -----------------------------------------------------------------------
361
+ const getThread: ToolDefinition = {
362
+ name: "get_thread",
363
+ description:
364
+ "Get all messages in a Gmail thread (including your sent replies). " +
365
+ "Use this to check if you already replied to a conversation before suggesting a new reply.",
366
+ parameters: z.object({
367
+ /** The Gmail thread ID (returned by search_emails or read_email). */
368
+ threadId: z.string().describe("Gmail thread ID"),
369
+ }),
370
+
371
+ handler: async (args) => {
372
+ try {
373
+ const threadId = args.threadId as string;
374
+ logger.debug({ threadId }, "get_thread called");
375
+
376
+ const res = await fetch(
377
+ `${GMAIL_API}/threads/${threadId}?format=metadata` +
378
+ "&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date",
379
+ { headers: await authHeaders(auth) },
380
+ );
381
+
382
+ if (!res.ok) {
383
+ const errText = await res.text();
384
+ logger.error({ status: res.status, errText }, "Gmail get_thread failed");
385
+ return `Error getting thread (${res.status}): ${errText}`;
386
+ }
387
+
388
+ const data = (await res.json()) as {
389
+ id: string;
390
+ messages: Array<{
391
+ id: string;
392
+ labelIds: string[];
393
+ snippet: string;
394
+ payload: { headers: Array<{ name: string; value: string }> };
395
+ }>;
396
+ };
397
+
398
+ const messages = data.messages.map((msg) => ({
399
+ id: msg.id,
400
+ from: getHeader(msg.payload.headers, "From"),
401
+ to: getHeader(msg.payload.headers, "To"),
402
+ date: getHeader(msg.payload.headers, "Date"),
403
+ snippet: msg.snippet,
404
+ isSent: msg.labelIds?.includes("SENT") ?? false,
405
+ }));
406
+
407
+ return {
408
+ threadId: data.id,
409
+ messageCount: messages.length,
410
+ messages,
411
+ };
412
+ } catch (error) {
413
+ const message = error instanceof Error ? error.message : String(error);
414
+ logger.error({ error: message }, "get_thread error");
415
+ return `Error getting thread: ${message}`;
416
+ }
417
+ },
418
+ };
419
+
420
+ // -----------------------------------------------------------------------
421
+ // search_threads (single-call inbox analysis)
422
+ // -----------------------------------------------------------------------
423
+ const searchThreads: ToolDefinition = {
424
+ name: "search_threads",
425
+ description:
426
+ "Search Gmail and return results grouped by thread. Each thread includes " +
427
+ "ALL messages (including your sent replies) with isSent flags, so you can " +
428
+ "tell at a glance whether you already replied. Ideal for inbox review — " +
429
+ "returns everything in a single call.",
430
+ parameters: z.object({
431
+ /** Gmail search query (e.g. "in:inbox", "from:alice"). */
432
+ query: z.string().describe("Gmail search query"),
433
+ /** Maximum threads to return (default 5, max 20). */
434
+ maxThreads: z
435
+ .number()
436
+ .int()
437
+ .min(1)
438
+ .max(20)
439
+ .optional()
440
+ .default(5)
441
+ .describe("Maximum number of threads to return"),
442
+ /** When true, includes the full decoded body of the latest message in
443
+ * each thread. When false, only snippets are returned. */
444
+ includeLatestBody: z
445
+ .boolean()
446
+ .optional()
447
+ .default(false)
448
+ .describe("Include full body of the latest message per thread"),
449
+ }),
450
+
451
+ handler: async (args) => {
452
+ try {
453
+ const query = args.query as string;
454
+ const maxThreads = (args.maxThreads as number | undefined) ?? 5;
455
+ const includeLatestBody = (args.includeLatestBody as boolean | undefined) ?? false;
456
+ logger.debug({ query, maxThreads, includeLatestBody }, "search_threads called");
457
+
458
+ // Step 1 — List thread IDs matching the query.
459
+ const params = new URLSearchParams({
460
+ q: query,
461
+ maxResults: String(maxThreads),
462
+ });
463
+ const listRes = await fetch(`${GMAIL_API}/threads?${params}`, {
464
+ headers: await authHeaders(auth),
465
+ });
466
+
467
+ if (!listRes.ok) {
468
+ const errText = await listRes.text();
469
+ logger.error({ status: listRes.status, errText }, "Gmail threads search failed");
470
+ return `Error searching threads (${listRes.status}): ${errText}`;
471
+ }
472
+
473
+ const listData = (await listRes.json()) as {
474
+ threads?: Array<{ id: string; snippet: string }>;
475
+ resultSizeEstimate?: number;
476
+ };
477
+
478
+ if (!listData.threads?.length) {
479
+ return { threadCount: 0, threads: [] };
480
+ }
481
+
482
+ // Step 2 — Fetch each thread. Use "full" format for the latest
483
+ // message body when requested, "metadata" otherwise.
484
+ const format = includeLatestBody ? "full" : "metadata";
485
+ const headers = await authHeaders(auth);
486
+ const threads = await Promise.all(
487
+ listData.threads.map(async (t) => {
488
+ const url =
489
+ `${GMAIL_API}/threads/${t.id}?format=${format}` +
490
+ "&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date";
491
+ const res = await fetch(url, { headers });
492
+
493
+ if (!res.ok) {
494
+ return { threadId: t.id, error: `Failed to fetch (${res.status})` };
495
+ }
496
+
497
+ const data = (await res.json()) as {
498
+ id: string;
499
+ messages: Array<{
500
+ id: string;
501
+ labelIds: string[];
502
+ snippet: string;
503
+ payload: {
504
+ headers: Array<{ name: string; value: string }>;
505
+ body?: { data?: string };
506
+ parts?: Array<Record<string, unknown>>;
507
+ mimeType?: string;
508
+ };
509
+ }>;
510
+ };
511
+
512
+ // Build a compact summary for each message in the thread.
513
+ const messages = data.messages.map((msg) => {
514
+ const result: Record<string, unknown> = {
515
+ id: msg.id,
516
+ from: getHeader(msg.payload.headers, "From"),
517
+ to: getHeader(msg.payload.headers, "To"),
518
+ date: getHeader(msg.payload.headers, "Date"),
519
+ snippet: msg.snippet,
520
+ isSent: msg.labelIds?.includes("SENT") ?? false,
521
+ };
522
+ return result;
523
+ });
524
+
525
+ // The last message in the array is the most recent.
526
+ const latest = data.messages[data.messages.length - 1];
527
+ const lastIsSent = latest?.labelIds?.includes("SENT") ?? false;
528
+ const subject = getHeader(
529
+ data.messages[0].payload.headers,
530
+ "Subject",
531
+ );
532
+
533
+ const thread: Record<string, unknown> = {
534
+ threadId: data.id,
535
+ subject,
536
+ messageCount: messages.length,
537
+ lastMessageIsSent: lastIsSent,
538
+ messages,
539
+ };
540
+
541
+ // Only decode the body for the latest message when requested.
542
+ if (includeLatestBody && latest) {
543
+ thread.latestBody = extractBody(
544
+ latest.payload as Record<string, unknown>,
545
+ );
546
+ }
547
+
548
+ return thread;
549
+ }),
550
+ );
551
+
552
+ return {
553
+ threadCount: threads.length,
554
+ threads,
555
+ };
556
+ } catch (error) {
557
+ const message = error instanceof Error ? error.message : String(error);
558
+ logger.error({ error: message }, "search_threads error");
559
+ return `Error searching threads: ${message}`;
560
+ }
561
+ },
562
+ };
563
+
564
+ return [searchEmails, readEmail, sendEmail, getThread, searchThreads];
336
565
  }
@@ -0,0 +1,361 @@
1
+ // plugins/google-calendar/auth.ts
2
+ var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
3
+ var EXPIRY_MARGIN_MS = 6e4;
4
+ var CalendarAuth = class {
5
+ clientId;
6
+ clientSecret;
7
+ refreshToken;
8
+ accessToken = null;
9
+ expiresAt = 0;
10
+ constructor(clientId, clientSecret, refreshToken) {
11
+ this.clientId = clientId;
12
+ this.clientSecret = clientSecret;
13
+ this.refreshToken = refreshToken;
14
+ }
15
+ /**
16
+ * Check whether all required credentials have been provided.
17
+ */
18
+ isConfigured() {
19
+ return Boolean(this.clientId && this.clientSecret && this.refreshToken);
20
+ }
21
+ /**
22
+ * Return a valid access token, refreshing it first if necessary.
23
+ *
24
+ * @throws {Error} If credentials are missing or the token exchange fails.
25
+ */
26
+ async getAccessToken() {
27
+ if (!this.isConfigured()) {
28
+ throw new Error(
29
+ "CalendarAuth is not configured \u2014 client ID, client secret, and refresh token are all required."
30
+ );
31
+ }
32
+ if (this.accessToken && Date.now() < this.expiresAt) {
33
+ return this.accessToken;
34
+ }
35
+ const body = new URLSearchParams({
36
+ client_id: this.clientId,
37
+ client_secret: this.clientSecret,
38
+ refresh_token: this.refreshToken,
39
+ grant_type: "refresh_token"
40
+ });
41
+ const response = await fetch(TOKEN_ENDPOINT, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
44
+ body: body.toString()
45
+ });
46
+ if (!response.ok) {
47
+ const text = await response.text();
48
+ throw new Error(
49
+ `Failed to refresh Google access token (${response.status}): ${text}`
50
+ );
51
+ }
52
+ const data = await response.json();
53
+ this.accessToken = data.access_token;
54
+ this.expiresAt = Date.now() + data.expires_in * 1e3 - EXPIRY_MARGIN_MS;
55
+ return this.accessToken;
56
+ }
57
+ };
58
+
59
+ // plugins/google-calendar/tools.ts
60
+ var CALENDAR_API = "https://www.googleapis.com/calendar/v3";
61
+ async function calendarFetch(auth, path, options = {}) {
62
+ const token = await auth.getAccessToken();
63
+ const res = await fetch(`${CALENDAR_API}${path}`, {
64
+ ...options,
65
+ headers: {
66
+ Authorization: `Bearer ${token}`,
67
+ "Content-Type": "application/json",
68
+ ...options.headers
69
+ }
70
+ });
71
+ if (!res.ok) {
72
+ const text = await res.text();
73
+ throw new Error(`Google Calendar API error (${res.status}): ${text}`);
74
+ }
75
+ if (res.status === 204) return { success: true };
76
+ return res.json();
77
+ }
78
+ function formatEventTime(dt) {
79
+ if (dt.dateTime) {
80
+ return new Date(dt.dateTime).toLocaleString("en-US", {
81
+ weekday: "short",
82
+ month: "short",
83
+ day: "numeric",
84
+ year: "numeric",
85
+ hour: "numeric",
86
+ minute: "2-digit",
87
+ timeZoneName: "short"
88
+ });
89
+ }
90
+ if (dt.date) {
91
+ return (/* @__PURE__ */ new Date(dt.date + "T00:00:00")).toLocaleDateString("en-US", {
92
+ weekday: "short",
93
+ month: "short",
94
+ day: "numeric",
95
+ year: "numeric"
96
+ });
97
+ }
98
+ return "N/A";
99
+ }
100
+ function formatEvent(event) {
101
+ const lines = [];
102
+ lines.push(`\u{1F4C5} ${event.summary ?? "(no title)"}`);
103
+ if (event.start) lines.push(` Start: ${formatEventTime(event.start)}`);
104
+ if (event.end) lines.push(` End: ${formatEventTime(event.end)}`);
105
+ if (event.location) lines.push(` \u{1F4CD} ${event.location}`);
106
+ if (event.description) lines.push(` ${event.description}`);
107
+ if (event.attendees?.length) {
108
+ const emails = event.attendees.map((a) => a.email).join(", ");
109
+ lines.push(` \u{1F465} ${emails}`);
110
+ }
111
+ lines.push(` ID: ${event.id}`);
112
+ return lines.join("\n");
113
+ }
114
+ function createCalendarTools(auth) {
115
+ const listEvents = {
116
+ name: "list_events",
117
+ description: "List upcoming calendar events. Optionally filter by date range.",
118
+ parameters: {
119
+ type: "object",
120
+ properties: {
121
+ maxResults: {
122
+ type: "number",
123
+ description: "Maximum number of events to return (default 10)."
124
+ },
125
+ timeMin: {
126
+ type: "string",
127
+ description: "Start of the time range as an ISO 8601 date-time string. Defaults to now."
128
+ },
129
+ timeMax: {
130
+ type: "string",
131
+ description: "End of the time range as an ISO 8601 date-time string."
132
+ },
133
+ calendarId: {
134
+ type: "string",
135
+ description: 'Calendar ID to query (default "primary").'
136
+ }
137
+ }
138
+ },
139
+ handler: async (args) => {
140
+ try {
141
+ const calendarId = encodeURIComponent(
142
+ args.calendarId ?? "primary"
143
+ );
144
+ const params = new URLSearchParams({
145
+ maxResults: String(args.maxResults ?? 10),
146
+ timeMin: args.timeMin ?? (/* @__PURE__ */ new Date()).toISOString(),
147
+ orderBy: "startTime",
148
+ singleEvents: "true"
149
+ });
150
+ if (args.timeMax) params.set("timeMax", args.timeMax);
151
+ const data = await calendarFetch(
152
+ auth,
153
+ `/calendars/${calendarId}/events?${params.toString()}`
154
+ );
155
+ const events = data.items ?? [];
156
+ if (events.length === 0) {
157
+ return "No upcoming events found.";
158
+ }
159
+ return events.map(formatEvent).join("\n\n");
160
+ } catch (error) {
161
+ return `Error listing events: ${error.message}`;
162
+ }
163
+ }
164
+ };
165
+ const createEvent = {
166
+ name: "create_event",
167
+ description: "Create a new calendar event.",
168
+ parameters: {
169
+ type: "object",
170
+ properties: {
171
+ summary: { type: "string", description: "Event title." },
172
+ description: { type: "string", description: "Event description." },
173
+ startTime: {
174
+ type: "string",
175
+ description: "Start date-time in ISO 8601 format."
176
+ },
177
+ endTime: {
178
+ type: "string",
179
+ description: "End date-time in ISO 8601 format."
180
+ },
181
+ location: { type: "string", description: "Event location." },
182
+ attendees: {
183
+ type: "array",
184
+ items: { type: "string" },
185
+ description: "List of attendee email addresses."
186
+ },
187
+ calendarId: {
188
+ type: "string",
189
+ description: 'Calendar ID (default "primary").'
190
+ }
191
+ },
192
+ required: ["summary", "startTime", "endTime"]
193
+ },
194
+ handler: async (args) => {
195
+ try {
196
+ const calendarId = encodeURIComponent(
197
+ args.calendarId ?? "primary"
198
+ );
199
+ const body = {
200
+ summary: args.summary,
201
+ start: { dateTime: args.startTime },
202
+ end: { dateTime: args.endTime }
203
+ };
204
+ if (args.description) body.description = args.description;
205
+ if (args.location) body.location = args.location;
206
+ if (args.attendees) {
207
+ body.attendees = args.attendees.map((email) => ({
208
+ email
209
+ }));
210
+ }
211
+ const event = await calendarFetch(
212
+ auth,
213
+ `/calendars/${calendarId}/events`,
214
+ { method: "POST", body: JSON.stringify(body) }
215
+ );
216
+ return `\u2705 Event created successfully!
217
+
218
+ ${formatEvent(event)}`;
219
+ } catch (error) {
220
+ return `Error creating event: ${error.message}`;
221
+ }
222
+ }
223
+ };
224
+ const updateEvent = {
225
+ name: "update_event",
226
+ description: "Update an existing calendar event by event ID.",
227
+ parameters: {
228
+ type: "object",
229
+ properties: {
230
+ eventId: { type: "string", description: "The event ID to update." },
231
+ summary: { type: "string", description: "New event title." },
232
+ description: { type: "string", description: "New event description." },
233
+ startTime: {
234
+ type: "string",
235
+ description: "New start date-time in ISO 8601 format."
236
+ },
237
+ endTime: {
238
+ type: "string",
239
+ description: "New end date-time in ISO 8601 format."
240
+ },
241
+ location: { type: "string", description: "New event location." },
242
+ calendarId: {
243
+ type: "string",
244
+ description: 'Calendar ID (default "primary").'
245
+ }
246
+ },
247
+ required: ["eventId"]
248
+ },
249
+ handler: async (args) => {
250
+ try {
251
+ const calendarId = encodeURIComponent(
252
+ args.calendarId ?? "primary"
253
+ );
254
+ const eventId = encodeURIComponent(args.eventId);
255
+ const body = {};
256
+ if (args.summary !== void 0) body.summary = args.summary;
257
+ if (args.description !== void 0)
258
+ body.description = args.description;
259
+ if (args.startTime !== void 0)
260
+ body.start = { dateTime: args.startTime };
261
+ if (args.endTime !== void 0) body.end = { dateTime: args.endTime };
262
+ if (args.location !== void 0) body.location = args.location;
263
+ const event = await calendarFetch(
264
+ auth,
265
+ `/calendars/${calendarId}/events/${eventId}`,
266
+ { method: "PATCH", body: JSON.stringify(body) }
267
+ );
268
+ return `\u2705 Event updated successfully!
269
+
270
+ ${formatEvent(event)}`;
271
+ } catch (error) {
272
+ return `Error updating event: ${error.message}`;
273
+ }
274
+ }
275
+ };
276
+ const deleteEvent = {
277
+ name: "delete_event",
278
+ description: "Delete a calendar event by event ID.",
279
+ parameters: {
280
+ type: "object",
281
+ properties: {
282
+ eventId: { type: "string", description: "The event ID to delete." },
283
+ calendarId: {
284
+ type: "string",
285
+ description: 'Calendar ID (default "primary").'
286
+ }
287
+ },
288
+ required: ["eventId"]
289
+ },
290
+ handler: async (args) => {
291
+ try {
292
+ const calendarId = encodeURIComponent(
293
+ args.calendarId ?? "primary"
294
+ );
295
+ const eventId = encodeURIComponent(args.eventId);
296
+ await calendarFetch(
297
+ auth,
298
+ `/calendars/${calendarId}/events/${eventId}`,
299
+ { method: "DELETE" }
300
+ );
301
+ return `\u2705 Event ${args.eventId} deleted successfully.`;
302
+ } catch (error) {
303
+ return `Error deleting event: ${error.message}`;
304
+ }
305
+ }
306
+ };
307
+ return [listEvents, createEvent, updateEvent, deleteEvent];
308
+ }
309
+
310
+ // plugins/google-calendar/index.ts
311
+ var createPlugin = () => {
312
+ let auth;
313
+ let tools = [];
314
+ let logger;
315
+ return {
316
+ id: "google-calendar",
317
+ name: "Google Calendar Plugin",
318
+ version: "1.0.0",
319
+ description: "View, create, and manage Google Calendar events",
320
+ requiredCredentials: [
321
+ "GCAL_CLIENT_ID",
322
+ "GCAL_CLIENT_SECRET",
323
+ "GCAL_REFRESH_TOKEN"
324
+ ],
325
+ async initialize(context) {
326
+ logger = context.logger;
327
+ logger.info("Initialising Google Calendar plugin\u2026");
328
+ auth = new CalendarAuth(
329
+ context.credentials.GCAL_CLIENT_ID,
330
+ context.credentials.GCAL_CLIENT_SECRET,
331
+ context.credentials.GCAL_REFRESH_TOKEN
332
+ );
333
+ if (!auth.isConfigured()) {
334
+ throw new Error(
335
+ "Google Calendar plugin is missing one or more required credentials."
336
+ );
337
+ }
338
+ tools = createCalendarTools(auth);
339
+ logger.info(`Registered ${tools.length} calendar tools`);
340
+ },
341
+ getTools() {
342
+ return tools;
343
+ },
344
+ async destroy() {
345
+ tools = [];
346
+ logger?.info("Google Calendar plugin destroyed");
347
+ },
348
+ async healthCheck() {
349
+ try {
350
+ await auth.getAccessToken();
351
+ return true;
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+ };
357
+ };
358
+ var index_default = createPlugin;
359
+ export {
360
+ index_default as default
361
+ };