@plosson/agentio 0.4.2 → 0.4.4
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 +4 -4
- package/package.json +3 -1
- package/src/auth/oauth.ts +22 -2
- package/src/commands/gateway.ts +259 -0
- package/src/commands/gcal.ts +383 -0
- package/src/commands/gsheets.ts +365 -0
- package/src/commands/gtasks.ts +326 -0
- package/src/commands/status.ts +85 -0
- package/src/commands/telegram.ts +209 -1
- package/src/commands/update.ts +2 -2
- package/src/commands/whatsapp.ts +853 -0
- package/src/config/config-manager.ts +1 -1
- package/src/gateway/adapters/telegram.ts +357 -0
- package/src/gateway/adapters/types.ts +147 -0
- package/src/gateway/adapters/whatsapp-auth.ts +172 -0
- package/src/gateway/adapters/whatsapp.ts +723 -0
- package/src/gateway/api.ts +791 -0
- package/src/gateway/client.ts +402 -0
- package/src/gateway/daemon.ts +461 -0
- package/src/gateway/store.ts +637 -0
- package/src/gateway/types.ts +325 -0
- package/src/gateway/webhook.ts +109 -0
- package/src/index.ts +34 -16
- package/src/polyfills.ts +10 -0
- package/src/services/gcal/client.ts +380 -0
- package/src/services/gsheets/client.ts +362 -0
- package/src/services/gtasks/client.ts +301 -0
- package/src/types/config.ts +37 -1
- package/src/types/gcal.ts +135 -0
- package/src/types/gsheets.ts +81 -0
- package/src/types/gtasks.ts +58 -0
- package/src/types/qrcode-terminal.d.ts +8 -0
- package/src/types/whatsapp.ts +116 -0
- package/src/utils/output.ts +586 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { google, calendar_v3 } from 'googleapis';
|
|
2
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
3
|
+
import type {
|
|
4
|
+
GCalCalendar,
|
|
5
|
+
GCalEvent,
|
|
6
|
+
GCalListOptions,
|
|
7
|
+
GCalCreateOptions,
|
|
8
|
+
GCalUpdateOptions,
|
|
9
|
+
GCalRespondOptions,
|
|
10
|
+
GCalFreeBusyOptions,
|
|
11
|
+
GCalFreeBusyResponse,
|
|
12
|
+
} from '../../types/gcal';
|
|
13
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
14
|
+
import { CliError } from '../../utils/errors';
|
|
15
|
+
|
|
16
|
+
export class GCalClient implements ServiceClient {
|
|
17
|
+
private calendar: calendar_v3.Calendar;
|
|
18
|
+
private userEmail: string | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(auth: OAuth2Client) {
|
|
21
|
+
this.calendar = google.calendar({ version: 'v3', auth });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async validate(): Promise<ValidationResult> {
|
|
25
|
+
try {
|
|
26
|
+
const email = await this.getUserEmail();
|
|
27
|
+
return { valid: true, info: email };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
30
|
+
if (message.includes('invalid_grant') || message.includes('Token has been expired or revoked')) {
|
|
31
|
+
return { valid: false, error: 'refresh token expired, re-authenticate' };
|
|
32
|
+
}
|
|
33
|
+
return { valid: false, error: message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async getUserEmail(): Promise<string> {
|
|
38
|
+
if (this.userEmail) return this.userEmail;
|
|
39
|
+
|
|
40
|
+
const response = await this.calendar.calendarList.get({ calendarId: 'primary' });
|
|
41
|
+
this.userEmail = response.data.id || 'me';
|
|
42
|
+
return this.userEmail;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async listCalendars(limit: number = 100): Promise<GCalCalendar[]> {
|
|
46
|
+
try {
|
|
47
|
+
const response = await this.calendar.calendarList.list({
|
|
48
|
+
maxResults: Math.min(limit, 250),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return (response.data.items || []).map((cal) => ({
|
|
52
|
+
id: cal.id!,
|
|
53
|
+
summary: cal.summary || '',
|
|
54
|
+
description: cal.description || undefined,
|
|
55
|
+
accessRole: cal.accessRole || '',
|
|
56
|
+
primary: cal.primary || false,
|
|
57
|
+
timeZone: cal.timeZone || undefined,
|
|
58
|
+
}));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
throw new CliError('API_ERROR', `Calendar API error: ${message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async listEvents(options: GCalListOptions = {}): Promise<{ events: GCalEvent[]; nextPageToken?: string }> {
|
|
66
|
+
const { calendarId = 'primary', timeMin, timeMax, maxResults = 10, pageToken, query, singleEvents = true, orderBy } = options;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const params: calendar_v3.Params$Resource$Events$List = {
|
|
70
|
+
calendarId,
|
|
71
|
+
maxResults: Math.min(maxResults, 250),
|
|
72
|
+
singleEvents,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (timeMin) params.timeMin = timeMin;
|
|
76
|
+
if (timeMax) params.timeMax = timeMax;
|
|
77
|
+
if (pageToken) params.pageToken = pageToken;
|
|
78
|
+
if (query) params.q = query;
|
|
79
|
+
if (singleEvents && orderBy) params.orderBy = orderBy;
|
|
80
|
+
else if (singleEvents) params.orderBy = 'startTime';
|
|
81
|
+
|
|
82
|
+
const response = await this.calendar.events.list(params);
|
|
83
|
+
|
|
84
|
+
const events: GCalEvent[] = (response.data.items || []).map((event) => this.parseEvent(event));
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
events,
|
|
88
|
+
nextPageToken: response.data.nextPageToken || undefined,
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
throw new CliError('API_ERROR', `Calendar API error: ${message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getEvent(calendarId: string, eventId: string): Promise<GCalEvent> {
|
|
97
|
+
try {
|
|
98
|
+
const response = await this.calendar.events.get({ calendarId, eventId });
|
|
99
|
+
return this.parseEvent(response.data);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (this.isNotFoundError(error)) {
|
|
102
|
+
throw new CliError('NOT_FOUND', `Event not found: ${eventId}`);
|
|
103
|
+
}
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
throw new CliError('API_ERROR', `Calendar API error: ${message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async createEvent(options: GCalCreateOptions): Promise<GCalEvent> {
|
|
110
|
+
const { calendarId = 'primary', summary, description, location, start, end, allDay, attendees, recurrence, reminders, colorId, visibility, transparency, sendUpdates = 'all', withMeet } = options;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const event: calendar_v3.Schema$Event = {
|
|
114
|
+
summary,
|
|
115
|
+
description,
|
|
116
|
+
location,
|
|
117
|
+
start: this.buildEventDateTime(start, allDay),
|
|
118
|
+
end: this.buildEventDateTime(end, allDay),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (attendees?.length) {
|
|
122
|
+
event.attendees = attendees.map((email) => ({ email }));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (recurrence?.length) {
|
|
126
|
+
event.recurrence = recurrence;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (reminders?.length) {
|
|
130
|
+
event.reminders = {
|
|
131
|
+
useDefault: false,
|
|
132
|
+
overrides: reminders.map((r) => ({ method: r.method, minutes: r.minutes })),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (colorId) event.colorId = colorId;
|
|
137
|
+
if (visibility) event.visibility = visibility;
|
|
138
|
+
if (transparency) event.transparency = transparency;
|
|
139
|
+
|
|
140
|
+
if (withMeet) {
|
|
141
|
+
event.conferenceData = {
|
|
142
|
+
createRequest: {
|
|
143
|
+
requestId: `agentio-${Date.now()}`,
|
|
144
|
+
conferenceSolutionKey: { type: 'hangoutsMeet' },
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const params: calendar_v3.Params$Resource$Events$Insert = {
|
|
150
|
+
calendarId,
|
|
151
|
+
requestBody: event,
|
|
152
|
+
sendUpdates,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (withMeet) {
|
|
156
|
+
params.conferenceDataVersion = 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const response = await this.calendar.events.insert(params);
|
|
160
|
+
return this.parseEvent(response.data);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
throw new CliError('API_ERROR', `Failed to create event: ${message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async updateEvent(options: GCalUpdateOptions): Promise<GCalEvent> {
|
|
168
|
+
const { calendarId = 'primary', eventId, summary, description, location, start, end, allDay, attendees, addAttendees, recurrence, reminders, colorId, visibility, transparency, sendUpdates = 'all' } = options;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// If addAttendees is used, fetch existing event first
|
|
172
|
+
let existingAttendees: calendar_v3.Schema$EventAttendee[] = [];
|
|
173
|
+
if (addAttendees?.length) {
|
|
174
|
+
const existing = await this.calendar.events.get({ calendarId, eventId });
|
|
175
|
+
existingAttendees = existing.data.attendees || [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const patch: calendar_v3.Schema$Event = {};
|
|
179
|
+
|
|
180
|
+
if (summary !== undefined) patch.summary = summary;
|
|
181
|
+
if (description !== undefined) patch.description = description;
|
|
182
|
+
if (location !== undefined) patch.location = location;
|
|
183
|
+
if (start) patch.start = this.buildEventDateTime(start, allDay);
|
|
184
|
+
if (end) patch.end = this.buildEventDateTime(end, allDay);
|
|
185
|
+
if (colorId !== undefined) patch.colorId = colorId;
|
|
186
|
+
if (visibility) patch.visibility = visibility;
|
|
187
|
+
if (transparency) patch.transparency = transparency;
|
|
188
|
+
|
|
189
|
+
if (attendees?.length) {
|
|
190
|
+
patch.attendees = attendees.map((email) => ({ email }));
|
|
191
|
+
} else if (addAttendees?.length) {
|
|
192
|
+
const existingEmails = new Set(existingAttendees.map((a) => a.email?.toLowerCase()));
|
|
193
|
+
const newAttendees = addAttendees
|
|
194
|
+
.filter((email) => !existingEmails.has(email.toLowerCase()))
|
|
195
|
+
.map((email) => ({ email, responseStatus: 'needsAction' }));
|
|
196
|
+
patch.attendees = [...existingAttendees, ...newAttendees];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (recurrence?.length) {
|
|
200
|
+
patch.recurrence = recurrence;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (reminders?.length) {
|
|
204
|
+
patch.reminders = {
|
|
205
|
+
useDefault: false,
|
|
206
|
+
overrides: reminders.map((r) => ({ method: r.method, minutes: r.minutes })),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const response = await this.calendar.events.patch({
|
|
211
|
+
calendarId,
|
|
212
|
+
eventId,
|
|
213
|
+
requestBody: patch,
|
|
214
|
+
sendUpdates,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return this.parseEvent(response.data);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (this.isNotFoundError(error)) {
|
|
220
|
+
throw new CliError('NOT_FOUND', `Event not found: ${eventId}`);
|
|
221
|
+
}
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
throw new CliError('API_ERROR', `Failed to update event: ${message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async deleteEvent(calendarId: string, eventId: string, sendUpdates: 'all' | 'externalOnly' | 'none' = 'all'): Promise<void> {
|
|
228
|
+
try {
|
|
229
|
+
await this.calendar.events.delete({ calendarId, eventId, sendUpdates });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (this.isNotFoundError(error)) {
|
|
232
|
+
throw new CliError('NOT_FOUND', `Event not found: ${eventId}`);
|
|
233
|
+
}
|
|
234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
235
|
+
throw new CliError('API_ERROR', `Failed to delete event: ${message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async respond(options: GCalRespondOptions): Promise<GCalEvent> {
|
|
240
|
+
const { calendarId = 'primary', eventId, status, comment } = options;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const event = await this.calendar.events.get({ calendarId, eventId });
|
|
244
|
+
|
|
245
|
+
if (!event.data.attendees?.length) {
|
|
246
|
+
throw new CliError('INVALID_PARAMS', 'Event has no attendees');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const selfIndex = event.data.attendees.findIndex((a) => a.self);
|
|
250
|
+
if (selfIndex === -1) {
|
|
251
|
+
throw new CliError('INVALID_PARAMS', 'You are not an attendee of this event');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (event.data.attendees[selfIndex].organizer) {
|
|
255
|
+
throw new CliError('INVALID_PARAMS', 'Cannot respond to your own event (you are the organizer)');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
event.data.attendees[selfIndex].responseStatus = status;
|
|
259
|
+
if (comment) {
|
|
260
|
+
event.data.attendees[selfIndex].comment = comment;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const response = await this.calendar.events.patch({
|
|
264
|
+
calendarId,
|
|
265
|
+
eventId,
|
|
266
|
+
requestBody: { attendees: event.data.attendees },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return this.parseEvent(response.data);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof CliError) throw error;
|
|
272
|
+
if (this.isNotFoundError(error)) {
|
|
273
|
+
throw new CliError('NOT_FOUND', `Event not found: ${eventId}`);
|
|
274
|
+
}
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
throw new CliError('API_ERROR', `Failed to respond to event: ${message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async freeBusy(options: GCalFreeBusyOptions): Promise<GCalFreeBusyResponse> {
|
|
281
|
+
const { calendarIds, timeMin, timeMax } = options;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const response = await this.calendar.freebusy.query({
|
|
285
|
+
requestBody: {
|
|
286
|
+
timeMin,
|
|
287
|
+
timeMax,
|
|
288
|
+
items: calendarIds.map((id) => ({ id })),
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const calendars: GCalFreeBusyResponse['calendars'] = {};
|
|
293
|
+
for (const [id, data] of Object.entries(response.data.calendars || {})) {
|
|
294
|
+
calendars[id] = {
|
|
295
|
+
busy: (data.busy || []).map((b) => ({ start: b.start!, end: b.end! })),
|
|
296
|
+
errors: data.errors?.map((e) => ({ domain: e.domain || '', reason: e.reason || '' })),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { calendars };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
303
|
+
throw new CliError('API_ERROR', `Calendar API error: ${message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async search(query: string, options: Omit<GCalListOptions, 'query'> = {}): Promise<{ events: GCalEvent[]; nextPageToken?: string }> {
|
|
308
|
+
return this.listEvents({ ...options, query });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private buildEventDateTime(value: string, allDay?: boolean): calendar_v3.Schema$EventDateTime {
|
|
312
|
+
const trimmed = value.trim();
|
|
313
|
+
if (allDay || !trimmed.includes('T')) {
|
|
314
|
+
return { date: trimmed };
|
|
315
|
+
}
|
|
316
|
+
return { dateTime: trimmed };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private parseEvent(event: calendar_v3.Schema$Event): GCalEvent {
|
|
320
|
+
return {
|
|
321
|
+
id: event.id!,
|
|
322
|
+
summary: event.summary || undefined,
|
|
323
|
+
description: event.description || undefined,
|
|
324
|
+
location: event.location || undefined,
|
|
325
|
+
start: {
|
|
326
|
+
dateTime: event.start?.dateTime || undefined,
|
|
327
|
+
date: event.start?.date || undefined,
|
|
328
|
+
timeZone: event.start?.timeZone || undefined,
|
|
329
|
+
},
|
|
330
|
+
end: {
|
|
331
|
+
dateTime: event.end?.dateTime || undefined,
|
|
332
|
+
date: event.end?.date || undefined,
|
|
333
|
+
timeZone: event.end?.timeZone || undefined,
|
|
334
|
+
},
|
|
335
|
+
status: event.status || undefined,
|
|
336
|
+
htmlLink: event.htmlLink || undefined,
|
|
337
|
+
created: event.created || undefined,
|
|
338
|
+
updated: event.updated || undefined,
|
|
339
|
+
colorId: event.colorId || undefined,
|
|
340
|
+
creator: event.creator ? { email: event.creator.email!, displayName: event.creator.displayName || undefined } : undefined,
|
|
341
|
+
organizer: event.organizer ? { email: event.organizer.email!, displayName: event.organizer.displayName || undefined } : undefined,
|
|
342
|
+
attendees: event.attendees?.map((a) => ({
|
|
343
|
+
email: a.email!,
|
|
344
|
+
displayName: a.displayName || undefined,
|
|
345
|
+
responseStatus: a.responseStatus || undefined,
|
|
346
|
+
optional: a.optional || undefined,
|
|
347
|
+
organizer: a.organizer || undefined,
|
|
348
|
+
self: a.self || undefined,
|
|
349
|
+
comment: a.comment || undefined,
|
|
350
|
+
})),
|
|
351
|
+
recurrence: event.recurrence || undefined,
|
|
352
|
+
recurringEventId: event.recurringEventId || undefined,
|
|
353
|
+
transparency: event.transparency || undefined,
|
|
354
|
+
visibility: event.visibility || undefined,
|
|
355
|
+
reminders: event.reminders ? {
|
|
356
|
+
useDefault: event.reminders.useDefault || false,
|
|
357
|
+
overrides: event.reminders.overrides?.map((r) => ({
|
|
358
|
+
method: r.method as 'email' | 'popup',
|
|
359
|
+
minutes: r.minutes!,
|
|
360
|
+
})),
|
|
361
|
+
} : undefined,
|
|
362
|
+
hangoutLink: event.hangoutLink || undefined,
|
|
363
|
+
conferenceData: event.conferenceData ? {
|
|
364
|
+
entryPoints: event.conferenceData.entryPoints?.map((ep) => ({
|
|
365
|
+
entryPointType: ep.entryPointType!,
|
|
366
|
+
uri: ep.uri!,
|
|
367
|
+
label: ep.label || undefined,
|
|
368
|
+
})),
|
|
369
|
+
} : undefined,
|
|
370
|
+
eventType: event.eventType || undefined,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private isNotFoundError(error: unknown): boolean {
|
|
375
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
376
|
+
return (error as { code: unknown }).code === 404;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|