@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.
- package/dist/cli/index.js +56 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +44 -9
- package/dist/index.js.map +1 -1
- package/heartbeats/email-reply-check.heartbeat.md +13 -14
- package/package.json +1 -1
- package/plugins/gmail/index.compiled.mjs +14207 -0
- package/plugins/gmail/plugin.json +1 -1
- package/plugins/gmail/tools.ts +240 -11
- package/plugins/google-calendar/index.compiled.mjs +361 -0
package/plugins/gmail/tools.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
170
|
-
`${GMAIL_API}/messages/${msg.id}?format
|
|
171
|
-
{
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|