@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,365 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { createGoogleAuth } from '../auth/token-manager';
|
|
5
|
+
import { setCredentials } from '../auth/token-store';
|
|
6
|
+
import { setProfile } from '../config/config-manager';
|
|
7
|
+
import { createProfileCommands } from '../utils/profile-commands';
|
|
8
|
+
import { createClientGetter } from '../utils/client-factory';
|
|
9
|
+
import { performOAuthFlow } from '../auth/oauth';
|
|
10
|
+
import { GSheetsClient } from '../services/gsheets/client';
|
|
11
|
+
import {
|
|
12
|
+
printGSheetsList,
|
|
13
|
+
printGSheetsMetadata,
|
|
14
|
+
printGSheetsValues,
|
|
15
|
+
printGSheetsUpdateResult,
|
|
16
|
+
printGSheetsAppendResult,
|
|
17
|
+
printGSheetsClearResult,
|
|
18
|
+
printGSheetsCreated,
|
|
19
|
+
} from '../utils/output';
|
|
20
|
+
import { CliError, handleError } from '../utils/errors';
|
|
21
|
+
import type { GSheetsCredentials } from '../types/gsheets';
|
|
22
|
+
|
|
23
|
+
const getGSheetsClient = createClientGetter<GSheetsCredentials, GSheetsClient>({
|
|
24
|
+
service: 'gsheets',
|
|
25
|
+
createClient: (credentials) => new GSheetsClient(credentials),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse values from CLI arguments or JSON
|
|
30
|
+
* Supports two formats:
|
|
31
|
+
* - Simple: comma-separated rows, pipe-separated cells (e.g., "a|b|c,d|e|f")
|
|
32
|
+
* - JSON: 2D array (e.g., '[["a","b"],["c","d"]]')
|
|
33
|
+
*/
|
|
34
|
+
function parseValues(valueArgs: string[], valuesJson?: string): unknown[][] {
|
|
35
|
+
if (valuesJson) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(valuesJson);
|
|
38
|
+
if (!Array.isArray(parsed)) {
|
|
39
|
+
throw new Error('JSON must be a 2D array');
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw new CliError('INVALID_PARAMS', `Invalid JSON values: ${err instanceof Error ? err.message : err}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (valueArgs.length === 0) {
|
|
48
|
+
throw new CliError('INVALID_PARAMS', 'No values provided', 'Provide values as args or via --values-json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse simple format: comma-separated rows, pipe-separated cells
|
|
52
|
+
const rawValues = valueArgs.join(' ');
|
|
53
|
+
const rows = rawValues.split(',');
|
|
54
|
+
return rows.map((row) => {
|
|
55
|
+
const cells = row.trim().split('|');
|
|
56
|
+
return cells.map((cell) => cell.trim());
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerGSheetsCommands(program: Command): void {
|
|
61
|
+
const gsheets = program.command('gsheets').description('Google Sheets operations');
|
|
62
|
+
|
|
63
|
+
gsheets
|
|
64
|
+
.command('list')
|
|
65
|
+
.description('List recent spreadsheets')
|
|
66
|
+
.option('--profile <name>', 'Profile name')
|
|
67
|
+
.option('--limit <n>', 'Number of spreadsheets', '10')
|
|
68
|
+
.option('--query <query>', 'Drive search query filter')
|
|
69
|
+
.addHelpText(
|
|
70
|
+
'after',
|
|
71
|
+
`
|
|
72
|
+
Query Syntax Examples:
|
|
73
|
+
|
|
74
|
+
Name search:
|
|
75
|
+
--query "name contains 'budget'" Spreadsheets with "budget" in name
|
|
76
|
+
--query "name = 'Q1 Report'" Exact name match
|
|
77
|
+
|
|
78
|
+
Ownership:
|
|
79
|
+
--query "'me' in owners" Spreadsheets you own
|
|
80
|
+
--query "'user@example.com' in owners"
|
|
81
|
+
|
|
82
|
+
Date filters:
|
|
83
|
+
--query "modifiedTime > '2024-01-01'" Modified after date
|
|
84
|
+
--query "createdTime > '2024-01-01'" Created after date
|
|
85
|
+
|
|
86
|
+
Combined:
|
|
87
|
+
--query "name contains 'sales' and modifiedTime > '2024-01-01'"
|
|
88
|
+
`
|
|
89
|
+
)
|
|
90
|
+
.action(async (options) => {
|
|
91
|
+
try {
|
|
92
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
93
|
+
const spreadsheets = await client.list({
|
|
94
|
+
limit: parseInt(options.limit, 10),
|
|
95
|
+
query: options.query,
|
|
96
|
+
});
|
|
97
|
+
printGSheetsList(spreadsheets);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
handleError(error);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
gsheets
|
|
104
|
+
.command('get')
|
|
105
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
106
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A1:B10)')
|
|
107
|
+
.description('Get values from a range')
|
|
108
|
+
.option('--profile <name>', 'Profile name')
|
|
109
|
+
.option('--dimension <dim>', 'Major dimension: ROWS or COLUMNS')
|
|
110
|
+
.option('--render <opt>', 'Value render: FORMATTED_VALUE, UNFORMATTED_VALUE, or FORMULA')
|
|
111
|
+
.action(async (spreadsheetId: string, range: string, options) => {
|
|
112
|
+
try {
|
|
113
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
114
|
+
const result = await client.get(spreadsheetId, range, {
|
|
115
|
+
majorDimension: options.dimension,
|
|
116
|
+
valueRenderOption: options.render,
|
|
117
|
+
});
|
|
118
|
+
printGSheetsValues(result);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
handleError(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
gsheets
|
|
125
|
+
.command('update')
|
|
126
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
127
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A1:B2)')
|
|
128
|
+
.argument('[values...]', 'Values (comma-separated rows, pipe-separated cells)')
|
|
129
|
+
.description('Update values in a range')
|
|
130
|
+
.option('--profile <name>', 'Profile name')
|
|
131
|
+
.option('--values-json <json>', 'Values as JSON 2D array')
|
|
132
|
+
.option('--input <opt>', 'Value input option: RAW or USER_ENTERED', 'USER_ENTERED')
|
|
133
|
+
.addHelpText(
|
|
134
|
+
'after',
|
|
135
|
+
`
|
|
136
|
+
Value Formats:
|
|
137
|
+
|
|
138
|
+
Simple format (comma = row separator, pipe = cell separator):
|
|
139
|
+
agentio gsheets update <id> Sheet1!A1:B2 "a|b,c|d"
|
|
140
|
+
Results in:
|
|
141
|
+
A1=a B1=b
|
|
142
|
+
A2=c B2=d
|
|
143
|
+
|
|
144
|
+
JSON format:
|
|
145
|
+
agentio gsheets update <id> Sheet1!A1:B2 --values-json '[["a","b"],["c","d"]]'
|
|
146
|
+
|
|
147
|
+
Input Options:
|
|
148
|
+
RAW - Values are stored exactly as entered (no parsing)
|
|
149
|
+
USER_ENTERED - Values are parsed as if typed in the UI (formulas, dates, etc.)
|
|
150
|
+
`
|
|
151
|
+
)
|
|
152
|
+
.action(async (spreadsheetId: string, range: string, valueArgs: string[], options) => {
|
|
153
|
+
try {
|
|
154
|
+
const values = parseValues(valueArgs, options.valuesJson);
|
|
155
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
156
|
+
const result = await client.update(spreadsheetId, range, values, {
|
|
157
|
+
valueInputOption: options.input,
|
|
158
|
+
});
|
|
159
|
+
printGSheetsUpdateResult(result);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
handleError(error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
gsheets
|
|
166
|
+
.command('append')
|
|
167
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
168
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A:C)')
|
|
169
|
+
.argument('[values...]', 'Values (comma-separated rows, pipe-separated cells)')
|
|
170
|
+
.description('Append values to a range')
|
|
171
|
+
.option('--profile <name>', 'Profile name')
|
|
172
|
+
.option('--values-json <json>', 'Values as JSON 2D array')
|
|
173
|
+
.option('--input <opt>', 'Value input option: RAW or USER_ENTERED', 'USER_ENTERED')
|
|
174
|
+
.option('--insert <opt>', 'Insert data option: OVERWRITE or INSERT_ROWS')
|
|
175
|
+
.addHelpText(
|
|
176
|
+
'after',
|
|
177
|
+
`
|
|
178
|
+
Value Formats:
|
|
179
|
+
|
|
180
|
+
Simple format (comma = row separator, pipe = cell separator):
|
|
181
|
+
agentio gsheets append <id> Sheet1!A:C "a|b|c,d|e|f"
|
|
182
|
+
Appends two rows to columns A, B, C
|
|
183
|
+
|
|
184
|
+
JSON format:
|
|
185
|
+
agentio gsheets append <id> Sheet1!A:C --values-json '[["a","b","c"],["d","e","f"]]'
|
|
186
|
+
|
|
187
|
+
Insert Options:
|
|
188
|
+
OVERWRITE - New data overwrites existing data in the areas it is written
|
|
189
|
+
INSERT_ROWS - Rows are inserted for the new data
|
|
190
|
+
`
|
|
191
|
+
)
|
|
192
|
+
.action(async (spreadsheetId: string, range: string, valueArgs: string[], options) => {
|
|
193
|
+
try {
|
|
194
|
+
const values = parseValues(valueArgs, options.valuesJson);
|
|
195
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
196
|
+
const result = await client.append(spreadsheetId, range, values, {
|
|
197
|
+
valueInputOption: options.input,
|
|
198
|
+
insertDataOption: options.insert,
|
|
199
|
+
});
|
|
200
|
+
printGSheetsAppendResult(result);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
handleError(error);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
gsheets
|
|
207
|
+
.command('clear')
|
|
208
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
209
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A1:B10)')
|
|
210
|
+
.description('Clear values in a range')
|
|
211
|
+
.option('--profile <name>', 'Profile name')
|
|
212
|
+
.action(async (spreadsheetId: string, range: string, options) => {
|
|
213
|
+
try {
|
|
214
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
215
|
+
const result = await client.clear(spreadsheetId, range);
|
|
216
|
+
printGSheetsClearResult(result);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
handleError(error);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
gsheets
|
|
223
|
+
.command('metadata')
|
|
224
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
225
|
+
.description('Get spreadsheet metadata')
|
|
226
|
+
.option('--profile <name>', 'Profile name')
|
|
227
|
+
.action(async (spreadsheetId: string, options) => {
|
|
228
|
+
try {
|
|
229
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
230
|
+
const metadata = await client.metadata(spreadsheetId);
|
|
231
|
+
printGSheetsMetadata(metadata);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
handleError(error);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
gsheets
|
|
238
|
+
.command('create')
|
|
239
|
+
.argument('<title>', 'Spreadsheet title')
|
|
240
|
+
.description('Create a new spreadsheet')
|
|
241
|
+
.option('--profile <name>', 'Profile name')
|
|
242
|
+
.option('--sheets <names>', 'Comma-separated sheet names to create')
|
|
243
|
+
.action(async (title: string, options) => {
|
|
244
|
+
try {
|
|
245
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
246
|
+
const sheetNames = options.sheets ? options.sheets.split(',').map((n: string) => n.trim()) : undefined;
|
|
247
|
+
const result = await client.create(title, sheetNames);
|
|
248
|
+
printGSheetsCreated(result);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
handleError(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
gsheets
|
|
255
|
+
.command('copy')
|
|
256
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
257
|
+
.argument('<title>', 'New spreadsheet title')
|
|
258
|
+
.description('Copy a spreadsheet')
|
|
259
|
+
.option('--profile <name>', 'Profile name')
|
|
260
|
+
.option('--parent <folder-id>', 'Destination folder ID')
|
|
261
|
+
.action(async (spreadsheetId: string, title: string, options) => {
|
|
262
|
+
try {
|
|
263
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
264
|
+
const result = await client.copy(spreadsheetId, title, options.parent);
|
|
265
|
+
printGSheetsCreated(result);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
handleError(error);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
gsheets
|
|
272
|
+
.command('export')
|
|
273
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
274
|
+
.description('Export a spreadsheet to a file')
|
|
275
|
+
.option('--profile <name>', 'Profile name')
|
|
276
|
+
.requiredOption('--output <path>', 'Output file path')
|
|
277
|
+
.option('--format <fmt>', 'Export format: xlsx, pdf, csv, ods, tsv', 'xlsx')
|
|
278
|
+
.addHelpText(
|
|
279
|
+
'after',
|
|
280
|
+
`
|
|
281
|
+
Export Formats:
|
|
282
|
+
xlsx - Microsoft Excel (default)
|
|
283
|
+
pdf - PDF document
|
|
284
|
+
csv - Comma-separated values (first sheet only)
|
|
285
|
+
ods - OpenDocument Spreadsheet
|
|
286
|
+
tsv - Tab-separated values (first sheet only)
|
|
287
|
+
|
|
288
|
+
Examples:
|
|
289
|
+
agentio gsheets export <id> --output report.xlsx
|
|
290
|
+
agentio gsheets export <id> --output data.csv --format csv
|
|
291
|
+
`
|
|
292
|
+
)
|
|
293
|
+
.action(async (spreadsheetId: string, options) => {
|
|
294
|
+
try {
|
|
295
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
296
|
+
const format = options.format.toLowerCase();
|
|
297
|
+
|
|
298
|
+
if (!['xlsx', 'pdf', 'csv', 'ods', 'tsv'].includes(format)) {
|
|
299
|
+
throw new CliError('INVALID_PARAMS', `Unknown format: ${format}`, 'Use xlsx, pdf, csv, ods, or tsv');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const result = await client.export(spreadsheetId, format as 'xlsx' | 'pdf' | 'csv' | 'ods' | 'tsv');
|
|
303
|
+
await writeFile(options.output, result.data);
|
|
304
|
+
console.log(`Exported to ${options.output}`);
|
|
305
|
+
console.log(` Format: ${format}`);
|
|
306
|
+
console.log(` Size: ${result.data.length} bytes`);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
handleError(error);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Profile management
|
|
313
|
+
const profile = createProfileCommands<GSheetsCredentials>(gsheets, {
|
|
314
|
+
service: 'gsheets',
|
|
315
|
+
displayName: 'Google Sheets',
|
|
316
|
+
getExtraInfo: (credentials) => (credentials?.email ? ` - ${credentials.email}` : ''),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
profile
|
|
320
|
+
.command('add')
|
|
321
|
+
.description('Add a new Google Sheets profile')
|
|
322
|
+
.option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
|
|
323
|
+
.action(async (options) => {
|
|
324
|
+
try {
|
|
325
|
+
console.error('Starting OAuth flow for Google Sheets...\n');
|
|
326
|
+
|
|
327
|
+
const tokens = await performOAuthFlow('gsheets');
|
|
328
|
+
const auth = createGoogleAuth(tokens);
|
|
329
|
+
|
|
330
|
+
// Fetch user email for profile naming
|
|
331
|
+
let userEmail: string;
|
|
332
|
+
try {
|
|
333
|
+
const oauth2 = google.oauth2({ version: 'v2', auth });
|
|
334
|
+
const userInfo = await oauth2.userinfo.get();
|
|
335
|
+
userEmail = userInfo.data.email || '';
|
|
336
|
+
if (!userEmail) {
|
|
337
|
+
throw new Error('No email returned');
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
341
|
+
throw new CliError('AUTH_FAILED', `Failed to fetch user email: ${errorMessage}`, 'Ensure the account has an email address');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const profileName = options.profile || userEmail;
|
|
345
|
+
|
|
346
|
+
const credentials: GSheetsCredentials = {
|
|
347
|
+
accessToken: tokens.access_token,
|
|
348
|
+
refreshToken: tokens.refresh_token,
|
|
349
|
+
expiryDate: tokens.expiry_date,
|
|
350
|
+
tokenType: tokens.token_type,
|
|
351
|
+
scope: tokens.scope,
|
|
352
|
+
email: userEmail,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
await setProfile('gsheets', profileName);
|
|
356
|
+
await setCredentials('gsheets', profileName, credentials);
|
|
357
|
+
|
|
358
|
+
console.log(`\nSuccess! Profile "${profileName}" configured.`);
|
|
359
|
+
console.log(` Email: ${userEmail}`);
|
|
360
|
+
console.log(` Test with: agentio gsheets list --profile ${profileName}`);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
handleError(error);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
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 { GTasksClient } from '../services/gtasks/client';
|
|
9
|
+
import {
|
|
10
|
+
printGTasksList,
|
|
11
|
+
printGTaskList,
|
|
12
|
+
printGTaskListCreated,
|
|
13
|
+
printGTaskListDeleted,
|
|
14
|
+
printGTasks,
|
|
15
|
+
printGTask,
|
|
16
|
+
printGTaskCreated,
|
|
17
|
+
printGTaskDeleted,
|
|
18
|
+
printGTasksCleared,
|
|
19
|
+
} from '../utils/output';
|
|
20
|
+
import { CliError, handleError } from '../utils/errors';
|
|
21
|
+
import { readStdin } from '../utils/stdin';
|
|
22
|
+
|
|
23
|
+
async function getGTasksClient(profileName?: string): Promise<{ client: GTasksClient; profile: string }> {
|
|
24
|
+
const { tokens, profile } = await getValidTokens('gtasks', profileName);
|
|
25
|
+
const auth = createGoogleAuth(tokens);
|
|
26
|
+
return { client: new GTasksClient(auth), profile };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerGTasksCommands(program: Command): void {
|
|
30
|
+
const gtasks = program
|
|
31
|
+
.command('gtasks')
|
|
32
|
+
.description('Google Tasks operations');
|
|
33
|
+
|
|
34
|
+
// === Task Lists Commands ===
|
|
35
|
+
|
|
36
|
+
const lists = gtasks
|
|
37
|
+
.command('lists')
|
|
38
|
+
.description('Manage task lists');
|
|
39
|
+
|
|
40
|
+
// List task lists (default subcommand)
|
|
41
|
+
lists
|
|
42
|
+
.command('list', { isDefault: true })
|
|
43
|
+
.description('List all task lists')
|
|
44
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
45
|
+
.option('--limit <n>', 'Max results', '100')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
try {
|
|
48
|
+
const { client } = await getGTasksClient(options.profile);
|
|
49
|
+
const result = await client.listTaskLists(parseInt(options.limit, 10));
|
|
50
|
+
printGTasksList(result.taskLists, result.nextPageToken);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
handleError(error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Create task list
|
|
57
|
+
lists
|
|
58
|
+
.command('create <title>')
|
|
59
|
+
.description('Create a new task list')
|
|
60
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
61
|
+
.action(async (title: string, options) => {
|
|
62
|
+
try {
|
|
63
|
+
const { client } = await getGTasksClient(options.profile);
|
|
64
|
+
const taskList = await client.createTaskList(title);
|
|
65
|
+
printGTaskListCreated(taskList);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
handleError(error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Delete task list
|
|
72
|
+
lists
|
|
73
|
+
.command('delete <tasklist-id>')
|
|
74
|
+
.description('Delete a task list')
|
|
75
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
76
|
+
.action(async (tasklistId: string, options) => {
|
|
77
|
+
try {
|
|
78
|
+
const { client } = await getGTasksClient(options.profile);
|
|
79
|
+
await client.deleteTaskList(tasklistId);
|
|
80
|
+
printGTaskListDeleted(tasklistId);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
handleError(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// === Task Commands ===
|
|
87
|
+
|
|
88
|
+
// List tasks in a task list
|
|
89
|
+
gtasks
|
|
90
|
+
.command('list <tasklist-id>')
|
|
91
|
+
.description('List tasks in a task list')
|
|
92
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
93
|
+
.option('--limit <n>', 'Max results', '20')
|
|
94
|
+
.option('--show-completed', 'Include completed tasks', true)
|
|
95
|
+
.option('--no-show-completed', 'Exclude completed tasks')
|
|
96
|
+
.option('--show-hidden', 'Include hidden tasks')
|
|
97
|
+
.option('--due-min <datetime>', 'Filter: due date minimum (RFC3339)')
|
|
98
|
+
.option('--due-max <datetime>', 'Filter: due date maximum (RFC3339)')
|
|
99
|
+
.action(async (tasklistId: string, options) => {
|
|
100
|
+
try {
|
|
101
|
+
const { client } = await getGTasksClient(options.profile);
|
|
102
|
+
const result = await client.listTasks({
|
|
103
|
+
tasklistId,
|
|
104
|
+
maxResults: parseInt(options.limit, 10),
|
|
105
|
+
showCompleted: options.showCompleted,
|
|
106
|
+
showHidden: options.showHidden,
|
|
107
|
+
dueMin: options.dueMin,
|
|
108
|
+
dueMax: options.dueMax,
|
|
109
|
+
});
|
|
110
|
+
printGTasks(result.tasks, result.nextPageToken);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
handleError(error);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Get a specific task
|
|
117
|
+
gtasks
|
|
118
|
+
.command('get <tasklist-id> <task-id>')
|
|
119
|
+
.description('Get a specific task')
|
|
120
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
121
|
+
.action(async (tasklistId: string, taskId: string, options) => {
|
|
122
|
+
try {
|
|
123
|
+
const { client } = await getGTasksClient(options.profile);
|
|
124
|
+
const task = await client.getTask(tasklistId, taskId);
|
|
125
|
+
printGTask(task);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
handleError(error);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Add a new task
|
|
132
|
+
gtasks
|
|
133
|
+
.command('add <tasklist-id>')
|
|
134
|
+
.description('Add a new task')
|
|
135
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
136
|
+
.requiredOption('--title <title>', 'Task title')
|
|
137
|
+
.option('--notes <text>', 'Task notes/description (or pipe via stdin)')
|
|
138
|
+
.option('--due <date>', 'Due date (RFC3339 or YYYY-MM-DD)')
|
|
139
|
+
.option('--parent <task-id>', 'Parent task ID (create as subtask)')
|
|
140
|
+
.option('--previous <task-id>', 'Previous sibling task ID (controls ordering)')
|
|
141
|
+
.action(async (tasklistId: string, options) => {
|
|
142
|
+
try {
|
|
143
|
+
let notes = options.notes;
|
|
144
|
+
if (!notes) {
|
|
145
|
+
const stdin = await readStdin();
|
|
146
|
+
if (stdin) notes = stdin;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { client } = await getGTasksClient(options.profile);
|
|
150
|
+
const task = await client.createTask({
|
|
151
|
+
tasklistId,
|
|
152
|
+
title: options.title,
|
|
153
|
+
notes,
|
|
154
|
+
due: options.due,
|
|
155
|
+
parent: options.parent,
|
|
156
|
+
previous: options.previous,
|
|
157
|
+
});
|
|
158
|
+
printGTaskCreated(task);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
handleError(error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Update a task
|
|
165
|
+
gtasks
|
|
166
|
+
.command('update <tasklist-id> <task-id>')
|
|
167
|
+
.description('Update an existing task')
|
|
168
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
169
|
+
.option('--title <title>', 'New title')
|
|
170
|
+
.option('--notes <text>', 'New notes (or pipe via stdin)')
|
|
171
|
+
.option('--due <date>', 'New due date (RFC3339 or YYYY-MM-DD, empty to clear)')
|
|
172
|
+
.option('--status <status>', 'New status: needsAction or completed')
|
|
173
|
+
.action(async (tasklistId: string, taskId: string, options) => {
|
|
174
|
+
try {
|
|
175
|
+
let notes = options.notes;
|
|
176
|
+
if (notes === undefined && !process.stdin.isTTY) {
|
|
177
|
+
const stdin = await readStdin();
|
|
178
|
+
if (stdin) notes = stdin;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (options.status && !['needsAction', 'completed'].includes(options.status)) {
|
|
182
|
+
throw new CliError('INVALID_PARAMS', `Invalid status: ${options.status}`, 'Use: needsAction or completed');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { client } = await getGTasksClient(options.profile);
|
|
186
|
+
const task = await client.updateTask({
|
|
187
|
+
tasklistId,
|
|
188
|
+
taskId,
|
|
189
|
+
title: options.title,
|
|
190
|
+
notes,
|
|
191
|
+
due: options.due,
|
|
192
|
+
status: options.status,
|
|
193
|
+
});
|
|
194
|
+
printGTask(task);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
handleError(error);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Mark task as done
|
|
201
|
+
gtasks
|
|
202
|
+
.command('done <tasklist-id> <task-id>')
|
|
203
|
+
.alias('complete')
|
|
204
|
+
.description('Mark a task as completed')
|
|
205
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
206
|
+
.action(async (tasklistId: string, taskId: string, options) => {
|
|
207
|
+
try {
|
|
208
|
+
const { client } = await getGTasksClient(options.profile);
|
|
209
|
+
const task = await client.completeTask(tasklistId, taskId);
|
|
210
|
+
console.log(`Task completed: ${task.title}`);
|
|
211
|
+
console.log(`ID: ${task.id}`);
|
|
212
|
+
console.log(`Status: ${task.status}`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
handleError(error);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Mark task as not done
|
|
219
|
+
gtasks
|
|
220
|
+
.command('undo <tasklist-id> <task-id>')
|
|
221
|
+
.alias('uncomplete')
|
|
222
|
+
.description('Mark a task as needs action (not completed)')
|
|
223
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
224
|
+
.action(async (tasklistId: string, taskId: string, options) => {
|
|
225
|
+
try {
|
|
226
|
+
const { client } = await getGTasksClient(options.profile);
|
|
227
|
+
const task = await client.uncompleteTask(tasklistId, taskId);
|
|
228
|
+
console.log(`Task uncompleted: ${task.title}`);
|
|
229
|
+
console.log(`ID: ${task.id}`);
|
|
230
|
+
console.log(`Status: ${task.status}`);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
handleError(error);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Delete a task
|
|
237
|
+
gtasks
|
|
238
|
+
.command('delete <tasklist-id> <task-id>')
|
|
239
|
+
.description('Delete a task')
|
|
240
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
241
|
+
.action(async (tasklistId: string, taskId: string, options) => {
|
|
242
|
+
try {
|
|
243
|
+
const { client } = await getGTasksClient(options.profile);
|
|
244
|
+
await client.deleteTask(tasklistId, taskId);
|
|
245
|
+
printGTaskDeleted(tasklistId, taskId);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
handleError(error);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Clear completed tasks
|
|
252
|
+
gtasks
|
|
253
|
+
.command('clear <tasklist-id>')
|
|
254
|
+
.description('Clear all completed tasks from a task list')
|
|
255
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
256
|
+
.action(async (tasklistId: string, options) => {
|
|
257
|
+
try {
|
|
258
|
+
const { client } = await getGTasksClient(options.profile);
|
|
259
|
+
await client.clearCompleted(tasklistId);
|
|
260
|
+
printGTasksCleared(tasklistId);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
handleError(error);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Move a task
|
|
267
|
+
gtasks
|
|
268
|
+
.command('move <tasklist-id> <task-id>')
|
|
269
|
+
.description('Move a task (change parent or position)')
|
|
270
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
271
|
+
.option('--parent <task-id>', 'New parent task ID (make subtask)')
|
|
272
|
+
.option('--previous <task-id>', 'Previous sibling task ID (change position)')
|
|
273
|
+
.action(async (tasklistId: string, taskId: string, options) => {
|
|
274
|
+
try {
|
|
275
|
+
if (!options.parent && !options.previous) {
|
|
276
|
+
throw new CliError('INVALID_PARAMS', 'At least one of --parent or --previous is required');
|
|
277
|
+
}
|
|
278
|
+
const { client } = await getGTasksClient(options.profile);
|
|
279
|
+
const task = await client.moveTask(tasklistId, taskId, options.parent, options.previous);
|
|
280
|
+
console.log(`Task moved: ${task.title}`);
|
|
281
|
+
console.log(`ID: ${task.id}`);
|
|
282
|
+
if (task.parent) console.log(`Parent: ${task.parent}`);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
handleError(error);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Profile management
|
|
289
|
+
const profile = createProfileCommands<{ email?: string }>(gtasks, {
|
|
290
|
+
service: 'gtasks',
|
|
291
|
+
displayName: 'Google Tasks',
|
|
292
|
+
getExtraInfo: (credentials) => credentials?.email ? ` - ${credentials.email}` : '',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
profile
|
|
296
|
+
.command('add')
|
|
297
|
+
.description('Add a new Google Tasks profile')
|
|
298
|
+
.option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
|
|
299
|
+
.action(async (options) => {
|
|
300
|
+
try {
|
|
301
|
+
console.error('Starting OAuth flow for Google Tasks...\n');
|
|
302
|
+
|
|
303
|
+
const tokens = await performOAuthFlow('gtasks');
|
|
304
|
+
|
|
305
|
+
// Fetch user email via oauth2 userinfo
|
|
306
|
+
const auth = createGoogleAuth(tokens);
|
|
307
|
+
const oauth2 = google.oauth2({ version: 'v2', auth });
|
|
308
|
+
const userInfo = await oauth2.userinfo.get();
|
|
309
|
+
const email = userInfo.data.email;
|
|
310
|
+
|
|
311
|
+
if (!email) {
|
|
312
|
+
throw new CliError('AUTH_FAILED', 'Could not fetch email', 'Try again or specify --profile manually');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const profileName = options.profile || email;
|
|
316
|
+
|
|
317
|
+
await setProfile('gtasks', profileName);
|
|
318
|
+
await setCredentials('gtasks', profileName, { ...tokens, email });
|
|
319
|
+
|
|
320
|
+
console.log(`\nSuccess! Profile "${profileName}" configured.`);
|
|
321
|
+
console.log(` Email: ${email}`);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
handleError(error);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|