@plosson/agentio 0.4.2 → 0.4.3
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 +14 -2
- package/src/commands/gateway.ts +259 -0
- package/src/commands/gcal.ts +383 -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 +32 -16
- package/src/polyfills.ts +10 -0
- package/src/services/gcal/client.ts +380 -0
- package/src/services/gtasks/client.ts +301 -0
- package/src/types/config.ts +36 -1
- package/src/types/gcal.ts +135 -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 +505 -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
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { google, tasks_v1 } from 'googleapis';
|
|
2
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
3
|
+
import type {
|
|
4
|
+
GTaskList,
|
|
5
|
+
GTask,
|
|
6
|
+
GTasksListOptions,
|
|
7
|
+
GTasksCreateOptions,
|
|
8
|
+
GTasksUpdateOptions,
|
|
9
|
+
} from '../../types/gtasks';
|
|
10
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
11
|
+
import { CliError } from '../../utils/errors';
|
|
12
|
+
|
|
13
|
+
export class GTasksClient implements ServiceClient {
|
|
14
|
+
private tasks: tasks_v1.Tasks;
|
|
15
|
+
private userEmail: string | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(auth: OAuth2Client) {
|
|
18
|
+
this.tasks = google.tasks({ version: 'v1', auth });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async validate(): Promise<ValidationResult> {
|
|
22
|
+
try {
|
|
23
|
+
// Try to list task lists to verify credentials work
|
|
24
|
+
await this.tasks.tasklists.list({ maxResults: 1 });
|
|
25
|
+
return { valid: true, info: 'tasks access ok' };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
28
|
+
if (message.includes('invalid_grant') || message.includes('Token has been expired or revoked')) {
|
|
29
|
+
return { valid: false, error: 'refresh token expired, re-authenticate' };
|
|
30
|
+
}
|
|
31
|
+
return { valid: false, error: message };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async listTaskLists(maxResults: number = 100, pageToken?: string): Promise<{ taskLists: GTaskList[]; nextPageToken?: string }> {
|
|
36
|
+
try {
|
|
37
|
+
const response = await this.tasks.tasklists.list({
|
|
38
|
+
maxResults: Math.min(maxResults, 100),
|
|
39
|
+
pageToken,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const taskLists: GTaskList[] = (response.data.items || []).map((tl) => ({
|
|
43
|
+
id: tl.id!,
|
|
44
|
+
title: tl.title || '',
|
|
45
|
+
updated: tl.updated || undefined,
|
|
46
|
+
selfLink: tl.selfLink || undefined,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
taskLists,
|
|
51
|
+
nextPageToken: response.data.nextPageToken || undefined,
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
throw new CliError('API_ERROR', `Tasks API error: ${message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async createTaskList(title: string): Promise<GTaskList> {
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.tasks.tasklists.insert({
|
|
62
|
+
requestBody: { title },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
id: response.data.id!,
|
|
67
|
+
title: response.data.title || '',
|
|
68
|
+
updated: response.data.updated || undefined,
|
|
69
|
+
selfLink: response.data.selfLink || undefined,
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new CliError('API_ERROR', `Failed to create task list: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async deleteTaskList(tasklistId: string): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
await this.tasks.tasklists.delete({ tasklist: tasklistId });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (this.isNotFoundError(error)) {
|
|
82
|
+
throw new CliError('NOT_FOUND', `Task list not found: ${tasklistId}`);
|
|
83
|
+
}
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
throw new CliError('API_ERROR', `Failed to delete task list: ${message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async listTasks(options: GTasksListOptions): Promise<{ tasks: GTask[]; nextPageToken?: string }> {
|
|
90
|
+
const {
|
|
91
|
+
tasklistId,
|
|
92
|
+
maxResults = 20,
|
|
93
|
+
pageToken,
|
|
94
|
+
showCompleted = true,
|
|
95
|
+
showDeleted = false,
|
|
96
|
+
showHidden = false,
|
|
97
|
+
dueMin,
|
|
98
|
+
dueMax,
|
|
99
|
+
completedMin,
|
|
100
|
+
completedMax,
|
|
101
|
+
updatedMin,
|
|
102
|
+
} = options;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const params: tasks_v1.Params$Resource$Tasks$List = {
|
|
106
|
+
tasklist: tasklistId,
|
|
107
|
+
maxResults: Math.min(maxResults, 100),
|
|
108
|
+
showCompleted,
|
|
109
|
+
showDeleted,
|
|
110
|
+
showHidden,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (pageToken) params.pageToken = pageToken;
|
|
114
|
+
if (dueMin) params.dueMin = dueMin;
|
|
115
|
+
if (dueMax) params.dueMax = dueMax;
|
|
116
|
+
if (completedMin) params.completedMin = completedMin;
|
|
117
|
+
if (completedMax) params.completedMax = completedMax;
|
|
118
|
+
if (updatedMin) params.updatedMin = updatedMin;
|
|
119
|
+
|
|
120
|
+
const response = await this.tasks.tasks.list(params);
|
|
121
|
+
|
|
122
|
+
const tasks: GTask[] = (response.data.items || []).map((t) => this.parseTask(t));
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
tasks,
|
|
126
|
+
nextPageToken: response.data.nextPageToken || undefined,
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (this.isNotFoundError(error)) {
|
|
130
|
+
throw new CliError('NOT_FOUND', `Task list not found: ${tasklistId}`);
|
|
131
|
+
}
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
throw new CliError('API_ERROR', `Tasks API error: ${message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getTask(tasklistId: string, taskId: string): Promise<GTask> {
|
|
138
|
+
try {
|
|
139
|
+
const response = await this.tasks.tasks.get({
|
|
140
|
+
tasklist: tasklistId,
|
|
141
|
+
task: taskId,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return this.parseTask(response.data);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (this.isNotFoundError(error)) {
|
|
147
|
+
throw new CliError('NOT_FOUND', `Task not found: ${taskId}`);
|
|
148
|
+
}
|
|
149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
+
throw new CliError('API_ERROR', `Tasks API error: ${message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async createTask(options: GTasksCreateOptions): Promise<GTask> {
|
|
155
|
+
const { tasklistId, title, notes, due, parent, previous } = options;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const task: tasks_v1.Schema$Task = {
|
|
159
|
+
title,
|
|
160
|
+
notes,
|
|
161
|
+
due: due ? this.normalizeDue(due) : undefined,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const params: tasks_v1.Params$Resource$Tasks$Insert = {
|
|
165
|
+
tasklist: tasklistId,
|
|
166
|
+
requestBody: task,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (parent) params.parent = parent;
|
|
170
|
+
if (previous) params.previous = previous;
|
|
171
|
+
|
|
172
|
+
const response = await this.tasks.tasks.insert(params);
|
|
173
|
+
|
|
174
|
+
return this.parseTask(response.data);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (this.isNotFoundError(error)) {
|
|
177
|
+
throw new CliError('NOT_FOUND', `Task list not found: ${tasklistId}`);
|
|
178
|
+
}
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
throw new CliError('API_ERROR', `Failed to create task: ${message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async updateTask(options: GTasksUpdateOptions): Promise<GTask> {
|
|
185
|
+
const { tasklistId, taskId, title, notes, due, status } = options;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const patch: tasks_v1.Schema$Task = {};
|
|
189
|
+
|
|
190
|
+
if (title !== undefined) patch.title = title;
|
|
191
|
+
if (notes !== undefined) patch.notes = notes;
|
|
192
|
+
if (due !== undefined) patch.due = due ? this.normalizeDue(due) : null;
|
|
193
|
+
if (status !== undefined) patch.status = status;
|
|
194
|
+
|
|
195
|
+
const response = await this.tasks.tasks.patch({
|
|
196
|
+
tasklist: tasklistId,
|
|
197
|
+
task: taskId,
|
|
198
|
+
requestBody: patch,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return this.parseTask(response.data);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (this.isNotFoundError(error)) {
|
|
204
|
+
throw new CliError('NOT_FOUND', `Task not found: ${taskId}`);
|
|
205
|
+
}
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
throw new CliError('API_ERROR', `Failed to update task: ${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async completeTask(tasklistId: string, taskId: string): Promise<GTask> {
|
|
212
|
+
return this.updateTask({ tasklistId, taskId, status: 'completed' });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async uncompleteTask(tasklistId: string, taskId: string): Promise<GTask> {
|
|
216
|
+
return this.updateTask({ tasklistId, taskId, status: 'needsAction' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async deleteTask(tasklistId: string, taskId: string): Promise<void> {
|
|
220
|
+
try {
|
|
221
|
+
await this.tasks.tasks.delete({
|
|
222
|
+
tasklist: tasklistId,
|
|
223
|
+
task: taskId,
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (this.isNotFoundError(error)) {
|
|
227
|
+
throw new CliError('NOT_FOUND', `Task not found: ${taskId}`);
|
|
228
|
+
}
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
throw new CliError('API_ERROR', `Failed to delete task: ${message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async clearCompleted(tasklistId: string): Promise<void> {
|
|
235
|
+
try {
|
|
236
|
+
await this.tasks.tasks.clear({ tasklist: tasklistId });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (this.isNotFoundError(error)) {
|
|
239
|
+
throw new CliError('NOT_FOUND', `Task list not found: ${tasklistId}`);
|
|
240
|
+
}
|
|
241
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
242
|
+
throw new CliError('API_ERROR', `Failed to clear completed tasks: ${message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async moveTask(tasklistId: string, taskId: string, parent?: string, previous?: string): Promise<GTask> {
|
|
247
|
+
try {
|
|
248
|
+
const params: tasks_v1.Params$Resource$Tasks$Move = {
|
|
249
|
+
tasklist: tasklistId,
|
|
250
|
+
task: taskId,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (parent) params.parent = parent;
|
|
254
|
+
if (previous) params.previous = previous;
|
|
255
|
+
|
|
256
|
+
const response = await this.tasks.tasks.move(params);
|
|
257
|
+
|
|
258
|
+
return this.parseTask(response.data);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (this.isNotFoundError(error)) {
|
|
261
|
+
throw new CliError('NOT_FOUND', `Task not found: ${taskId}`);
|
|
262
|
+
}
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
throw new CliError('API_ERROR', `Failed to move task: ${message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private normalizeDue(due: string): string {
|
|
269
|
+
// If it's already RFC3339, return as-is
|
|
270
|
+
if (due.includes('T')) {
|
|
271
|
+
return due;
|
|
272
|
+
}
|
|
273
|
+
// If it's just a date (YYYY-MM-DD), convert to RFC3339 with midnight UTC
|
|
274
|
+
return `${due}T00:00:00.000Z`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private parseTask(task: tasks_v1.Schema$Task): GTask {
|
|
278
|
+
return {
|
|
279
|
+
id: task.id!,
|
|
280
|
+
title: task.title || '',
|
|
281
|
+
status: (task.status as 'needsAction' | 'completed') || 'needsAction',
|
|
282
|
+
notes: task.notes || undefined,
|
|
283
|
+
due: task.due || undefined,
|
|
284
|
+
completed: task.completed || undefined,
|
|
285
|
+
parent: task.parent || undefined,
|
|
286
|
+
position: task.position || undefined,
|
|
287
|
+
updated: task.updated || undefined,
|
|
288
|
+
selfLink: task.selfLink || undefined,
|
|
289
|
+
webViewLink: task.webViewLink || undefined,
|
|
290
|
+
hidden: task.hidden || undefined,
|
|
291
|
+
deleted: task.deleted || undefined,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private isNotFoundError(error: unknown): boolean {
|
|
296
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
297
|
+
return (error as { code: unknown }).code === 404;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|