@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,383 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { getValidTokens, createGoogleAuth } from '../auth/token-manager';
|
|
4
|
+
import { setCredentials } from '../auth/token-store';
|
|
5
|
+
import { setProfile } from '../config/config-manager';
|
|
6
|
+
import { createProfileCommands } from '../utils/profile-commands';
|
|
7
|
+
import { performOAuthFlow } from '../auth/oauth';
|
|
8
|
+
import { GCalClient } from '../services/gcal/client';
|
|
9
|
+
import { printGCalCalendarList, printGCalEventList, printGCalEvent, printGCalEventCreated, printGCalEventDeleted, printGCalFreeBusy } from '../utils/output';
|
|
10
|
+
import { CliError, handleError } from '../utils/errors';
|
|
11
|
+
import { readStdin } from '../utils/stdin';
|
|
12
|
+
|
|
13
|
+
async function getGCalClient(profileName?: string): Promise<{ client: GCalClient; profile: string }> {
|
|
14
|
+
const { tokens, profile } = await getValidTokens('gcal', profileName);
|
|
15
|
+
const auth = createGoogleAuth(tokens);
|
|
16
|
+
return { client: new GCalClient(auth), profile };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseTimeRange(options: { from?: string; to?: string; today?: boolean; tomorrow?: boolean; days?: string }): { timeMin?: string; timeMax?: string } {
|
|
20
|
+
const now = new Date();
|
|
21
|
+
let timeMin: string | undefined;
|
|
22
|
+
let timeMax: string | undefined;
|
|
23
|
+
|
|
24
|
+
if (options.today) {
|
|
25
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
26
|
+
const end = new Date(start);
|
|
27
|
+
end.setDate(end.getDate() + 1);
|
|
28
|
+
timeMin = start.toISOString();
|
|
29
|
+
timeMax = end.toISOString();
|
|
30
|
+
} else if (options.tomorrow) {
|
|
31
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
32
|
+
const end = new Date(start);
|
|
33
|
+
end.setDate(end.getDate() + 1);
|
|
34
|
+
timeMin = start.toISOString();
|
|
35
|
+
timeMax = end.toISOString();
|
|
36
|
+
} else if (options.days) {
|
|
37
|
+
const days = parseInt(options.days, 10);
|
|
38
|
+
if (!isNaN(days) && days > 0) {
|
|
39
|
+
timeMin = now.toISOString();
|
|
40
|
+
const end = new Date(now);
|
|
41
|
+
end.setDate(end.getDate() + days);
|
|
42
|
+
timeMax = end.toISOString();
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
if (options.from) timeMin = options.from;
|
|
46
|
+
if (options.to) timeMax = options.to;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { timeMin, timeMax };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function registerGCalCommands(program: Command): void {
|
|
53
|
+
const gcal = program
|
|
54
|
+
.command('gcal')
|
|
55
|
+
.description('Google Calendar operations');
|
|
56
|
+
|
|
57
|
+
// List calendars
|
|
58
|
+
gcal
|
|
59
|
+
.command('calendars')
|
|
60
|
+
.description('List available calendars')
|
|
61
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
62
|
+
.option('--limit <n>', 'Max results', '100')
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
try {
|
|
65
|
+
const { client } = await getGCalClient(options.profile);
|
|
66
|
+
const calendars = await client.listCalendars(parseInt(options.limit, 10));
|
|
67
|
+
printGCalCalendarList(calendars);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
handleError(error);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// List events
|
|
74
|
+
gcal
|
|
75
|
+
.command('events')
|
|
76
|
+
.alias('list')
|
|
77
|
+
.description('List events from a calendar')
|
|
78
|
+
.argument('[calendar-id]', 'Calendar ID (default: primary)')
|
|
79
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
80
|
+
.option('--limit <n>', 'Max results', '10')
|
|
81
|
+
.option('--from <datetime>', 'Start time (RFC3339 or YYYY-MM-DD)')
|
|
82
|
+
.option('--to <datetime>', 'End time (RFC3339 or YYYY-MM-DD)')
|
|
83
|
+
.option('--today', 'Show today\'s events only')
|
|
84
|
+
.option('--tomorrow', 'Show tomorrow\'s events only')
|
|
85
|
+
.option('--days <n>', 'Show events for next N days')
|
|
86
|
+
.option('--query <q>', 'Free text search query')
|
|
87
|
+
.action(async (calendarId: string | undefined, options) => {
|
|
88
|
+
try {
|
|
89
|
+
const { client } = await getGCalClient(options.profile);
|
|
90
|
+
const { timeMin, timeMax } = parseTimeRange(options);
|
|
91
|
+
const result = await client.listEvents({
|
|
92
|
+
calendarId: calendarId || 'primary',
|
|
93
|
+
maxResults: parseInt(options.limit, 10),
|
|
94
|
+
timeMin,
|
|
95
|
+
timeMax,
|
|
96
|
+
query: options.query,
|
|
97
|
+
});
|
|
98
|
+
printGCalEventList(result.events, result.nextPageToken);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
handleError(error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Get event
|
|
105
|
+
gcal
|
|
106
|
+
.command('get')
|
|
107
|
+
.alias('event')
|
|
108
|
+
.description('Get a single event')
|
|
109
|
+
.argument('<calendar-id>', 'Calendar ID')
|
|
110
|
+
.argument('<event-id>', 'Event ID')
|
|
111
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
112
|
+
.action(async (calendarId: string, eventId: string, options) => {
|
|
113
|
+
try {
|
|
114
|
+
const { client } = await getGCalClient(options.profile);
|
|
115
|
+
const event = await client.getEvent(calendarId, eventId);
|
|
116
|
+
printGCalEvent(event);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
handleError(error);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Create event
|
|
123
|
+
gcal
|
|
124
|
+
.command('create')
|
|
125
|
+
.description('Create a new event')
|
|
126
|
+
.argument('[calendar-id]', 'Calendar ID (default: primary)')
|
|
127
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
128
|
+
.requiredOption('--summary <title>', 'Event title/summary')
|
|
129
|
+
.requiredOption('--from <datetime>', 'Start time (RFC3339 or YYYY-MM-DD for all-day)')
|
|
130
|
+
.requiredOption('--to <datetime>', 'End time (RFC3339 or YYYY-MM-DD for all-day)')
|
|
131
|
+
.option('--description <text>', 'Event description (or pipe via stdin)')
|
|
132
|
+
.option('--location <place>', 'Event location')
|
|
133
|
+
.option('--all-day', 'Create as all-day event')
|
|
134
|
+
.option('--attendee <email>', 'Attendee email (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
135
|
+
.option('--rrule <rule>', 'Recurrence rule (repeatable, e.g., RRULE:FREQ=WEEKLY;BYDAY=MO)', (val, acc: string[]) => [...acc, val], [])
|
|
136
|
+
.option('--reminder <spec>', 'Reminder as method:minutes (repeatable, e.g., popup:30, email:1440)', (val, acc: string[]) => [...acc, val], [])
|
|
137
|
+
.option('--color <id>', 'Color ID (1-11)')
|
|
138
|
+
.option('--visibility <v>', 'Visibility: default, public, private, confidential')
|
|
139
|
+
.option('--show-as <v>', 'Show as: busy, free')
|
|
140
|
+
.option('--send-updates <mode>', 'Send notifications: all, externalOnly, none', 'all')
|
|
141
|
+
.option('--with-meet', 'Create Google Meet link')
|
|
142
|
+
.action(async (calendarId: string | undefined, options) => {
|
|
143
|
+
try {
|
|
144
|
+
let description = options.description;
|
|
145
|
+
if (!description) {
|
|
146
|
+
const stdin = await readStdin();
|
|
147
|
+
if (stdin) description = stdin;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const reminders = options.reminder.map((r: string) => {
|
|
151
|
+
const [method, minutes] = r.split(':');
|
|
152
|
+
if (!method || !minutes || !['email', 'popup'].includes(method)) {
|
|
153
|
+
throw new CliError('INVALID_PARAMS', `Invalid reminder format: ${r}`, 'Use format: method:minutes (e.g., popup:30)');
|
|
154
|
+
}
|
|
155
|
+
return { method: method as 'email' | 'popup', minutes: parseInt(minutes, 10) };
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { client } = await getGCalClient(options.profile);
|
|
159
|
+
const event = await client.createEvent({
|
|
160
|
+
calendarId: calendarId || 'primary',
|
|
161
|
+
summary: options.summary,
|
|
162
|
+
description,
|
|
163
|
+
location: options.location,
|
|
164
|
+
start: options.from,
|
|
165
|
+
end: options.to,
|
|
166
|
+
allDay: options.allDay,
|
|
167
|
+
attendees: options.attendee.length ? options.attendee : undefined,
|
|
168
|
+
recurrence: options.rrule.length ? options.rrule : undefined,
|
|
169
|
+
reminders: reminders.length ? reminders : undefined,
|
|
170
|
+
colorId: options.color,
|
|
171
|
+
visibility: options.visibility,
|
|
172
|
+
transparency: options.showAs === 'free' ? 'transparent' : options.showAs === 'busy' ? 'opaque' : undefined,
|
|
173
|
+
sendUpdates: options.sendUpdates,
|
|
174
|
+
withMeet: options.withMeet,
|
|
175
|
+
});
|
|
176
|
+
printGCalEventCreated(event);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
handleError(error);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Update event
|
|
183
|
+
gcal
|
|
184
|
+
.command('update')
|
|
185
|
+
.description('Update an existing event')
|
|
186
|
+
.argument('<calendar-id>', 'Calendar ID')
|
|
187
|
+
.argument('<event-id>', 'Event ID')
|
|
188
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
189
|
+
.option('--summary <title>', 'New event title/summary')
|
|
190
|
+
.option('--from <datetime>', 'New start time')
|
|
191
|
+
.option('--to <datetime>', 'New end time')
|
|
192
|
+
.option('--description <text>', 'New description (or pipe via stdin)')
|
|
193
|
+
.option('--location <place>', 'New location')
|
|
194
|
+
.option('--all-day', 'Convert to all-day event')
|
|
195
|
+
.option('--attendee <email>', 'Replace attendees (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
196
|
+
.option('--add-attendee <email>', 'Add attendee (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
197
|
+
.option('--color <id>', 'New color ID (1-11)')
|
|
198
|
+
.option('--visibility <v>', 'Visibility: default, public, private, confidential')
|
|
199
|
+
.option('--show-as <v>', 'Show as: busy, free')
|
|
200
|
+
.option('--send-updates <mode>', 'Send notifications: all, externalOnly, none', 'all')
|
|
201
|
+
.action(async (calendarId: string, eventId: string, options) => {
|
|
202
|
+
try {
|
|
203
|
+
let description = options.description;
|
|
204
|
+
if (description === undefined && !process.stdin.isTTY) {
|
|
205
|
+
const stdin = await readStdin();
|
|
206
|
+
if (stdin) description = stdin;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (options.attendee.length && options.addAttendee.length) {
|
|
210
|
+
throw new CliError('INVALID_PARAMS', 'Cannot use both --attendee and --add-attendee');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const { client } = await getGCalClient(options.profile);
|
|
214
|
+
const event = await client.updateEvent({
|
|
215
|
+
calendarId,
|
|
216
|
+
eventId,
|
|
217
|
+
summary: options.summary,
|
|
218
|
+
description,
|
|
219
|
+
location: options.location,
|
|
220
|
+
start: options.from,
|
|
221
|
+
end: options.to,
|
|
222
|
+
allDay: options.allDay,
|
|
223
|
+
attendees: options.attendee.length ? options.attendee : undefined,
|
|
224
|
+
addAttendees: options.addAttendee.length ? options.addAttendee : undefined,
|
|
225
|
+
colorId: options.color,
|
|
226
|
+
visibility: options.visibility,
|
|
227
|
+
transparency: options.showAs === 'free' ? 'transparent' : options.showAs === 'busy' ? 'opaque' : undefined,
|
|
228
|
+
sendUpdates: options.sendUpdates,
|
|
229
|
+
});
|
|
230
|
+
printGCalEvent(event);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
handleError(error);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Delete event
|
|
237
|
+
gcal
|
|
238
|
+
.command('delete')
|
|
239
|
+
.description('Delete an event')
|
|
240
|
+
.argument('<calendar-id>', 'Calendar ID')
|
|
241
|
+
.argument('<event-id>', 'Event ID')
|
|
242
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
243
|
+
.option('--send-updates <mode>', 'Send notifications: all, externalOnly, none', 'all')
|
|
244
|
+
.action(async (calendarId: string, eventId: string, options) => {
|
|
245
|
+
try {
|
|
246
|
+
const { client } = await getGCalClient(options.profile);
|
|
247
|
+
await client.deleteEvent(calendarId, eventId, options.sendUpdates);
|
|
248
|
+
printGCalEventDeleted(calendarId, eventId);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
handleError(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Search events
|
|
255
|
+
gcal
|
|
256
|
+
.command('search')
|
|
257
|
+
.description('Search events')
|
|
258
|
+
.argument('<query>', 'Search query')
|
|
259
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
260
|
+
.option('--calendar <id>', 'Calendar ID', 'primary')
|
|
261
|
+
.option('--limit <n>', 'Max results', '25')
|
|
262
|
+
.option('--from <datetime>', 'Start time (RFC3339)')
|
|
263
|
+
.option('--to <datetime>', 'End time (RFC3339)')
|
|
264
|
+
.action(async (query: string, options) => {
|
|
265
|
+
try {
|
|
266
|
+
const { client } = await getGCalClient(options.profile);
|
|
267
|
+
|
|
268
|
+
// Default search range: 30 days past to 90 days future
|
|
269
|
+
const now = new Date();
|
|
270
|
+
const defaultFrom = new Date(now);
|
|
271
|
+
defaultFrom.setDate(defaultFrom.getDate() - 30);
|
|
272
|
+
const defaultTo = new Date(now);
|
|
273
|
+
defaultTo.setDate(defaultTo.getDate() + 90);
|
|
274
|
+
|
|
275
|
+
const result = await client.search(query, {
|
|
276
|
+
calendarId: options.calendar,
|
|
277
|
+
maxResults: parseInt(options.limit, 10),
|
|
278
|
+
timeMin: options.from || defaultFrom.toISOString(),
|
|
279
|
+
timeMax: options.to || defaultTo.toISOString(),
|
|
280
|
+
});
|
|
281
|
+
printGCalEventList(result.events, result.nextPageToken);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
handleError(error);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Respond to event
|
|
288
|
+
gcal
|
|
289
|
+
.command('respond')
|
|
290
|
+
.description('Respond to an event invitation')
|
|
291
|
+
.argument('<calendar-id>', 'Calendar ID')
|
|
292
|
+
.argument('<event-id>', 'Event ID')
|
|
293
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
294
|
+
.requiredOption('--status <status>', 'Response: accepted, declined, tentative')
|
|
295
|
+
.option('--comment <text>', 'Optional comment')
|
|
296
|
+
.action(async (calendarId: string, eventId: string, options) => {
|
|
297
|
+
try {
|
|
298
|
+
const status = options.status.toLowerCase();
|
|
299
|
+
if (!['accepted', 'declined', 'tentative'].includes(status)) {
|
|
300
|
+
throw new CliError('INVALID_PARAMS', `Invalid status: ${options.status}`, 'Use: accepted, declined, or tentative');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const { client } = await getGCalClient(options.profile);
|
|
304
|
+
const event = await client.respond({
|
|
305
|
+
calendarId,
|
|
306
|
+
eventId,
|
|
307
|
+
status: status as 'accepted' | 'declined' | 'tentative',
|
|
308
|
+
comment: options.comment,
|
|
309
|
+
});
|
|
310
|
+
console.log(`Response updated: ${status}`);
|
|
311
|
+
console.log(`Event: ${event.summary || '(no title)'}`);
|
|
312
|
+
if (event.htmlLink) console.log(`Link: ${event.htmlLink}`);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
handleError(error);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Free/busy query
|
|
319
|
+
gcal
|
|
320
|
+
.command('freebusy')
|
|
321
|
+
.description('Get free/busy information')
|
|
322
|
+
.argument('<calendar-ids>', 'Comma-separated calendar IDs')
|
|
323
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
324
|
+
.requiredOption('--from <datetime>', 'Start time (RFC3339)')
|
|
325
|
+
.requiredOption('--to <datetime>', 'End time (RFC3339)')
|
|
326
|
+
.action(async (calendarIds: string, options) => {
|
|
327
|
+
try {
|
|
328
|
+
const ids = calendarIds.split(',').map((id) => id.trim()).filter(Boolean);
|
|
329
|
+
if (ids.length === 0) {
|
|
330
|
+
throw new CliError('INVALID_PARAMS', 'At least one calendar ID is required');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const { client } = await getGCalClient(options.profile);
|
|
334
|
+
const result = await client.freeBusy({
|
|
335
|
+
calendarIds: ids,
|
|
336
|
+
timeMin: options.from,
|
|
337
|
+
timeMax: options.to,
|
|
338
|
+
});
|
|
339
|
+
printGCalFreeBusy(result);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
handleError(error);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Profile management
|
|
346
|
+
const profile = createProfileCommands<{ email?: string }>(gcal, {
|
|
347
|
+
service: 'gcal',
|
|
348
|
+
displayName: 'Google Calendar',
|
|
349
|
+
getExtraInfo: (credentials) => credentials?.email ? ` - ${credentials.email}` : '',
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
profile
|
|
353
|
+
.command('add')
|
|
354
|
+
.description('Add a new Google Calendar profile')
|
|
355
|
+
.option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
|
|
356
|
+
.action(async (options) => {
|
|
357
|
+
try {
|
|
358
|
+
console.error('Starting OAuth flow for Google Calendar...\n');
|
|
359
|
+
|
|
360
|
+
const tokens = await performOAuthFlow('gcal');
|
|
361
|
+
|
|
362
|
+
// Fetch the user's email from calendar settings
|
|
363
|
+
const auth = createGoogleAuth(tokens);
|
|
364
|
+
const calendar = google.calendar({ version: 'v3', auth });
|
|
365
|
+
const settings = await calendar.calendarList.get({ calendarId: 'primary' });
|
|
366
|
+
const email = settings.data.id;
|
|
367
|
+
|
|
368
|
+
if (!email) {
|
|
369
|
+
throw new CliError('AUTH_FAILED', 'Could not fetch email from Calendar', 'Try again or specify --profile manually');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const profileName = options.profile || email;
|
|
373
|
+
|
|
374
|
+
await setProfile('gcal', profileName);
|
|
375
|
+
await setCredentials('gcal', profileName, { ...tokens, email });
|
|
376
|
+
|
|
377
|
+
console.log(`\nSuccess! Profile "${profileName}" configured.`);
|
|
378
|
+
console.log(` Email: ${email}`);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
handleError(error);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|