m365-agent-cli 1.2.0

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.
Files changed (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,2092 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { Command } from 'commander';
4
+ import { resolveAuth } from '../lib/auth.js';
5
+ import { getEmail } from '../lib/ews-client.js';
6
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
7
+ import {
8
+ addChecklistItem,
9
+ addLinkedResource,
10
+ createTask,
11
+ createTaskFileAttachment,
12
+ createTaskLinkedResource,
13
+ createTaskReferenceAttachment,
14
+ createTodoList,
15
+ deleteAttachment,
16
+ deleteChecklistItem,
17
+ deleteTask,
18
+ deleteTaskLinkedResource,
19
+ deleteTaskOpenExtension,
20
+ deleteTodoList,
21
+ deleteTodoListOpenExtension,
22
+ getChecklistItem,
23
+ getTask,
24
+ getTaskAttachment,
25
+ getTaskAttachmentContent,
26
+ getTaskLinkedResource,
27
+ getTaskOpenExtension,
28
+ getTasks,
29
+ getTodoList,
30
+ getTodoListOpenExtension,
31
+ getTodoLists,
32
+ getTodoTasksDeltaPage,
33
+ listAttachments,
34
+ listTaskChecklistItems,
35
+ listTaskLinkedResources,
36
+ listTaskOpenExtensions,
37
+ listTodoListOpenExtensions,
38
+ removeLinkedResourceByWebUrl,
39
+ setTaskOpenExtension,
40
+ setTodoListOpenExtension,
41
+ type TodoImportance,
42
+ type TodoLinkedResource,
43
+ type TodoList,
44
+ type TodoStatus,
45
+ type TodoTask,
46
+ type TodoTasksQueryOptions,
47
+ updateChecklistItem,
48
+ updateTask,
49
+ updateTaskLinkedResource,
50
+ updateTaskOpenExtension,
51
+ updateTodoList,
52
+ updateTodoListOpenExtension,
53
+ uploadLargeFileAttachment
54
+ } from '../lib/todo-client.js';
55
+ import { checkReadOnly } from '../lib/utils.js';
56
+
57
+ function fmtDate(iso: string | undefined): string {
58
+ if (!iso) return '';
59
+ try {
60
+ return new Date(iso).toLocaleString('en-US', {
61
+ timeZone: 'UTC',
62
+ month: 'short',
63
+ day: 'numeric',
64
+ year: 'numeric',
65
+ hour: '2-digit',
66
+ minute: '2-digit',
67
+ hour12: false
68
+ });
69
+ } catch {
70
+ return iso;
71
+ }
72
+ }
73
+
74
+ function fmtDT(d: { dateTime: string; timeZone: string } | undefined): string {
75
+ if (!d) return '';
76
+ try {
77
+ return new Date(d.dateTime).toLocaleString('en-US', {
78
+ timeZone: d.timeZone || 'UTC',
79
+ month: 'short',
80
+ day: 'numeric',
81
+ year: 'numeric',
82
+ hour: '2-digit',
83
+ minute: '2-digit',
84
+ hour12: false
85
+ });
86
+ } catch {
87
+ return d.dateTime;
88
+ }
89
+ }
90
+
91
+ function impEmoji(i: TodoImportance | undefined): string {
92
+ return i === 'high' ? '\u{1F534}' : i === 'low' ? '\u{1F535}' : '\u26AA';
93
+ }
94
+ function stsEmoji(s: TodoStatus | undefined): string {
95
+ switch (s) {
96
+ case 'completed':
97
+ return '\u2705';
98
+ case 'inProgress':
99
+ return '\u{1F504}';
100
+ case 'waitingOnOthers':
101
+ return '\u23F3';
102
+ case 'deferred':
103
+ return '\u{1F4E6}';
104
+ case 'notStarted':
105
+ return '\u2B1B';
106
+ default:
107
+ return '\u26AA';
108
+ }
109
+ }
110
+
111
+ function emailUrl(id: string): string {
112
+ return `https://outlook.office365.com/mail/${encodeURIComponent(id)}`;
113
+ }
114
+
115
+ function linkedTitle(lr: Pick<TodoLinkedResource, 'displayName' | 'description'>): string {
116
+ const t = (lr.displayName || lr.description || '').trim();
117
+ return t || '(link)';
118
+ }
119
+
120
+ async function resolveListId(
121
+ token: string,
122
+ nameOrId: string,
123
+ user?: string
124
+ ): Promise<{ listId: string; listDisplay: string }> {
125
+ const listsR = await getTodoLists(token, user);
126
+ if (!listsR.ok || !listsR.data) {
127
+ console.error(`Error: ${listsR.error?.message}`);
128
+ process.exit(1);
129
+ }
130
+
131
+ const matched = listsR.data.find(
132
+ (l) =>
133
+ l.id === nameOrId ||
134
+ l.displayName.toLowerCase() === nameOrId.toLowerCase() ||
135
+ l.wellknownListName?.toLowerCase() === nameOrId.toLowerCase()
136
+ );
137
+
138
+ if (matched) {
139
+ return { listId: matched.id, listDisplay: matched.displayName };
140
+ }
141
+
142
+ const s = await getTodoList(token, nameOrId, user);
143
+ if (!s.ok || !s.data) {
144
+ console.error(`List not found: "${nameOrId}".`);
145
+ console.error('Use "m365-agent-cli todo lists".');
146
+ process.exit(1);
147
+ }
148
+ return { listId: s.data.id, listDisplay: s.data.displayName };
149
+ }
150
+
151
+ export const todoCommand = new Command('todo').description('Manage Microsoft To-Do tasks');
152
+
153
+ todoCommand
154
+ .command('lists')
155
+ .description('List all To-Do task lists')
156
+ .option('--json', 'Output as JSON')
157
+ .option('--token <token>', 'Use a specific token')
158
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
159
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
160
+ .action(async (opts: { json?: boolean; token?: string; identity?: string; user?: string }) => {
161
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
162
+ if (!auth.success) {
163
+ console.error(`Auth error: ${auth.error}`);
164
+ process.exit(1);
165
+ }
166
+ const result = await getTodoLists(auth.token!, opts.user);
167
+ if (!result.ok || !result.data) {
168
+ console.error(`Error: ${result.error?.message}`);
169
+ process.exit(1);
170
+ }
171
+ if (opts.json) {
172
+ console.log(JSON.stringify(result.data, null, 2));
173
+ return;
174
+ }
175
+ const lists: TodoList[] = result.data;
176
+ if (lists.length === 0) {
177
+ console.log('No task lists found.');
178
+ return;
179
+ }
180
+ console.log(`\nTo-Do Lists (${lists.length}):\n`);
181
+ for (const l of lists) {
182
+ const tag = l.isShared ? ' [shared]' : l.isOwner === false ? ' [shared with me]' : '';
183
+ console.log(` ${l.displayName}${tag}`);
184
+ console.log(` ID: ${l.id}`);
185
+ if (l.wellknownListName) console.log(` Well-known: ${l.wellknownListName}`);
186
+ console.log('');
187
+ }
188
+ });
189
+
190
+ todoCommand
191
+ .command('get')
192
+ .description('List tasks in a list, or show a single task')
193
+ .option('-l, --list <name|id>', 'List name or ID (default: Tasks)', 'Tasks')
194
+ .option('-t, --task <id>', 'Show detail for a specific task ID')
195
+ .option('--status <status>', 'Filter by status: notStarted, inProgress, completed, waitingOnOthers, deferred')
196
+ .option('--importance <importance>', 'Filter by importance: low, normal, high')
197
+ .option('--filter <odata>', 'Raw OData $filter (not combined with --status / --importance; see Graph todoTask)')
198
+ .option('--orderby <expr>', 'OData $orderby (e.g. lastModifiedDateTime desc)')
199
+ .option('--select <fields>', 'OData $select (comma-separated field names)')
200
+ .option('--top <n>', 'Page size; when set, returns a single page (no auto follow nextLink)')
201
+ .option('--skip <n>', 'OData $skip (single-page request; combine with --top for paging)')
202
+ .option('--expand <expr>', 'OData $expand (e.g. attachments)')
203
+ .option('--count', 'Add $count=true (single-page response)')
204
+ .option('--json', 'Output as JSON')
205
+ .option('--token <token>', 'Use a specific token')
206
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
207
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
208
+ .action(
209
+ async (opts: {
210
+ list?: string;
211
+ task?: string;
212
+ status?: string;
213
+ importance?: string;
214
+ filter?: string;
215
+ orderby?: string;
216
+ select?: string;
217
+ top?: string;
218
+ skip?: string;
219
+ expand?: string;
220
+ count?: boolean;
221
+ json?: boolean;
222
+ token?: string;
223
+ identity?: string;
224
+ user?: string;
225
+ }) => {
226
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
227
+ if (!auth.success) {
228
+ console.error(`Auth error: ${auth.error}`);
229
+ process.exit(1);
230
+ }
231
+
232
+ const listName = opts.list || 'Tasks';
233
+ const { listId, listDisplay } = await resolveListId(auth.token!, listName, opts.user);
234
+
235
+ if (opts.task) {
236
+ const r = await getTask(
237
+ auth.token!,
238
+ listId,
239
+ opts.task,
240
+ opts.user,
241
+ opts.select ? { select: opts.select } : undefined
242
+ );
243
+ if (!r.ok || !r.data) {
244
+ console.error(`Error: ${r.error?.message}`);
245
+ process.exit(1);
246
+ }
247
+ const t: TodoTask = r.data;
248
+ if (opts.json) {
249
+ console.log(JSON.stringify(t, null, 2));
250
+ return;
251
+ }
252
+ const hr = '\u2500'.repeat(60);
253
+ console.log(`\n${hr}`);
254
+ console.log(`Title: ${t.title}`);
255
+ console.log(`Status: ${stsEmoji(t.status)} ${t.status}`);
256
+ console.log(`Importance: ${impEmoji(t.importance)} ${t.importance}`);
257
+ if (t.categories?.length) console.log(`Categories: ${t.categories.join(', ')}`);
258
+ if (t.dueDateTime) console.log(`Due: ${fmtDT(t.dueDateTime)} (${t.dueDateTime.timeZone})`);
259
+ if (t.startDateTime) console.log(`Start: ${fmtDT(t.startDateTime)} (${t.startDateTime.timeZone})`);
260
+ if (t.isReminderOn && t.reminderDateTime) console.log(`Reminder: ${fmtDT(t.reminderDateTime)}`);
261
+ if (t.completedDateTime) console.log(`Completed: ${fmtDT(t.completedDateTime)}`);
262
+ if (t.linkedResources?.length) {
263
+ console.log('Linked:');
264
+ for (const lr of t.linkedResources) console.log(` - ${linkedTitle(lr)}: ${lr.webUrl ?? ''}`);
265
+ }
266
+ if (t.body?.content) {
267
+ console.log(`\n${hr}\n${t.body.content}`);
268
+ }
269
+ if (t.checklistItems?.length) {
270
+ console.log('\nChecklist:');
271
+ for (const item of t.checklistItems)
272
+ console.log(` ${item.isChecked ? '\u2611' : '\u2610'} ${item.displayName}`);
273
+ }
274
+ console.log(`\n${hr}`);
275
+ console.log(`ID: ${t.id}`);
276
+ if (t.createdDateTime) console.log(`Created: ${fmtDate(t.createdDateTime)}`);
277
+ if (t.lastModifiedDateTime) console.log(`Modified: ${fmtDate(t.lastModifiedDateTime)}`);
278
+ console.log('');
279
+ return;
280
+ }
281
+
282
+ if (opts.filter && (opts.status || opts.importance)) {
283
+ console.error('Error: use either --filter or --status/--importance, not both');
284
+ process.exit(1);
285
+ }
286
+
287
+ let listQuery: TodoTasksQueryOptions | string | undefined;
288
+ const parseTopSkip = (q: TodoTasksQueryOptions) => {
289
+ if (opts.top !== undefined) {
290
+ const n = parseInt(opts.top, 10);
291
+ if (Number.isNaN(n) || n < 1) {
292
+ console.error('Error: --top must be a positive integer');
293
+ process.exit(1);
294
+ }
295
+ q.top = n;
296
+ }
297
+ if (opts.skip !== undefined) {
298
+ const s = parseInt(opts.skip, 10);
299
+ if (Number.isNaN(s) || s < 0) {
300
+ console.error('Error: --skip must be a non-negative integer');
301
+ process.exit(1);
302
+ }
303
+ q.skip = s;
304
+ }
305
+ if (opts.expand) q.expand = opts.expand;
306
+ if (opts.count) q.count = true;
307
+ };
308
+
309
+ if (opts.filter) {
310
+ const q: TodoTasksQueryOptions = { filter: opts.filter };
311
+ if (opts.orderby) q.orderby = opts.orderby;
312
+ if (opts.select) q.select = opts.select;
313
+ parseTopSkip(q);
314
+ listQuery = q;
315
+ } else {
316
+ const filters: string[] = [];
317
+ if (opts.status) {
318
+ const validStatuses: TodoStatus[] = ['notStarted', 'inProgress', 'completed', 'waitingOnOthers', 'deferred'];
319
+ if (!validStatuses.includes(opts.status as TodoStatus)) {
320
+ console.error(`Error: Invalid status "${opts.status}". Valid values: ${validStatuses.join(', ')}`);
321
+ process.exit(1);
322
+ }
323
+ filters.push(`status eq '${opts.status}'`);
324
+ }
325
+ if (opts.importance) {
326
+ const validImportance: TodoImportance[] = ['low', 'normal', 'high'];
327
+ if (!validImportance.includes(opts.importance as TodoImportance)) {
328
+ console.error(
329
+ `Error: Invalid importance "${opts.importance}". Valid values: ${validImportance.join(', ')}`
330
+ );
331
+ process.exit(1);
332
+ }
333
+ filters.push(`importance eq '${opts.importance}'`);
334
+ }
335
+ const filterStr = filters.join(' and ') || undefined;
336
+ if (
337
+ filterStr ||
338
+ opts.orderby ||
339
+ opts.select ||
340
+ opts.top !== undefined ||
341
+ opts.skip !== undefined ||
342
+ opts.expand ||
343
+ opts.count
344
+ ) {
345
+ const q: TodoTasksQueryOptions = {};
346
+ if (filterStr) q.filter = filterStr;
347
+ if (opts.orderby) q.orderby = opts.orderby;
348
+ if (opts.select) q.select = opts.select;
349
+ parseTopSkip(q);
350
+ listQuery = q;
351
+ }
352
+ }
353
+
354
+ const result = await getTasks(auth.token!, listId, listQuery, opts.user);
355
+ if (!result.ok || !result.data) {
356
+ console.error(`Error: ${result.error?.message}`);
357
+ process.exit(1);
358
+ }
359
+ const tasks: TodoTask[] = result.data;
360
+ if (opts.json) {
361
+ console.log(JSON.stringify({ list: listDisplay, listId, tasks }, null, 2));
362
+ return;
363
+ }
364
+ if (tasks.length === 0) {
365
+ console.log(`\n${listDisplay}: no tasks found.\n`);
366
+ return;
367
+ }
368
+ console.log(`\n${listDisplay} (${tasks.length} task${tasks.length === 1 ? '' : 's'}):\n`);
369
+ for (const t of tasks) {
370
+ const due = t.dueDateTime ? `\u{1F4C5} ${fmtDT(t.dueDateTime)}` : '';
371
+ console.log(` ${t.status === 'completed' ? '\u2705' : ' '} ${impEmoji(t.importance)} ${t.title} ${due}`);
372
+ console.log(` ID: ${t.id} | ${t.status || 'no status'} | ${t.importance || 'normal'}`);
373
+ if (t.categories?.length) console.log(` Categories: ${t.categories.join(', ')}`);
374
+ if (t.linkedResources?.length)
375
+ console.log(` \u21B3 linked: ${t.linkedResources.map((l) => linkedTitle(l)).join(', ')}`);
376
+ console.log('');
377
+ }
378
+ }
379
+ );
380
+
381
+ todoCommand
382
+ .command('create')
383
+ .description('Create a new task')
384
+ .requiredOption('-t, --title <text>', 'Task title')
385
+ .option('-l, --list <name|id>', 'List name or ID (default: Tasks)', 'Tasks')
386
+ .option('-b, --body <text>', 'Task body/notes')
387
+ .option('-d, --due <ISO-8601>', 'Due date (e.g. 2026-04-15T17:00:00Z)')
388
+ .option('--start <ISO-8601>', 'Start date/time')
389
+ .option('--importance <level>', 'Importance: low, normal, high', 'normal')
390
+ .option('--status <status>', 'Initial status: notStarted, inProgress, waitingOnOthers, deferred', 'notStarted')
391
+ .option('--reminder <ISO-8601>', 'Reminder datetime')
392
+ .option('--timezone <tz>', 'Default time zone for due/start/reminder (e.g. UTC, Eastern Standard Time)', 'UTC')
393
+ .option('--due-tz <tz>', 'Time zone for due only (overrides --timezone)')
394
+ .option('--start-tz <tz>', 'Time zone for start only')
395
+ .option('--reminder-tz <tz>', 'Time zone for reminder only')
396
+ .option('--link <msgId>', 'Link task to an email by message ID')
397
+ .option('--mailbox <email>', 'Delegated or shared mailbox (with --link, for EWS message lookup)')
398
+ .option(
399
+ '--category <name>',
400
+ 'Category label (repeatable; To Do uses string categories)',
401
+ (v: string, prev: string[]) => [...prev, v],
402
+ [] as string[]
403
+ )
404
+ .option('--recurrence-json <path>', 'JSON file: Graph patternedRecurrence object')
405
+ .option('--json', 'Output as JSON')
406
+ .option('--token <token>', 'Use a specific token')
407
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
408
+ .option('--ews-identity <name>', 'EWS token cache identity for --link (default: default)')
409
+ .option('--user <email>', 'Target user or shared mailbox for the task (Graph delegation)')
410
+ .action(
411
+ async (
412
+ opts: {
413
+ title: string;
414
+ list?: string;
415
+ body?: string;
416
+ due?: string;
417
+ start?: string;
418
+ importance?: string;
419
+ status?: string;
420
+ reminder?: string;
421
+ timezone?: string;
422
+ dueTz?: string;
423
+ startTz?: string;
424
+ reminderTz?: string;
425
+ link?: string;
426
+ mailbox?: string;
427
+ category?: string[];
428
+ recurrenceJson?: string;
429
+ json?: boolean;
430
+ token?: string;
431
+ identity?: string;
432
+ ewsIdentity?: string;
433
+ user?: string;
434
+ },
435
+ cmd: any
436
+ ) => {
437
+ checkReadOnly(cmd);
438
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
439
+ if (!auth.success) {
440
+ console.error(`Auth error: ${auth.error}`);
441
+ process.exit(1);
442
+ }
443
+
444
+ const listName = opts.list || 'Tasks';
445
+ const { listId } = await resolveListId(auth.token!, listName, opts.user);
446
+
447
+ let recurrence: Record<string, unknown> | undefined;
448
+ if (opts.recurrenceJson) {
449
+ const raw = await readFile(opts.recurrenceJson, 'utf-8');
450
+ recurrence = JSON.parse(raw) as Record<string, unknown>;
451
+ }
452
+
453
+ let linkedResources: any[] | undefined;
454
+ if (opts.link) {
455
+ // Do not pass the Graph --token to EWS auth, as they require different tokens
456
+ const ewsAuth = await resolveAuth({ identity: opts.ewsIdentity });
457
+ if (!ewsAuth.success) {
458
+ console.error(`EWS Auth error: ${ewsAuth.error}`);
459
+ process.exit(1);
460
+ }
461
+ const er = await getEmail(ewsAuth.token!, opts.link, opts.mailbox);
462
+ if (!er.ok || !er.data) {
463
+ console.error(`Could not fetch email: ${er.error?.message}`);
464
+ process.exit(1);
465
+ }
466
+ linkedResources = [{ webUrl: emailUrl(er.data.Id), displayName: er.data.Subject || 'Linked email' }];
467
+ }
468
+
469
+ const cats = (opts.category ?? []).map((c) => c.trim()).filter(Boolean);
470
+ const result = await createTask(
471
+ auth.token!,
472
+ listId,
473
+ {
474
+ title: opts.title,
475
+ body: opts.body,
476
+ importance: opts.importance as TodoImportance,
477
+ status: opts.status as TodoStatus,
478
+ dueDateTime: opts.due,
479
+ startDateTime: opts.start,
480
+ reminderDateTime: opts.reminder,
481
+ timeZone: opts.timezone,
482
+ dueTimeZone: opts.dueTz,
483
+ startTimeZone: opts.startTz,
484
+ reminderTimeZone: opts.reminderTz,
485
+ isReminderOn: !!opts.reminder,
486
+ linkedResources,
487
+ categories: cats.length ? cats : undefined,
488
+ recurrence
489
+ },
490
+ opts.user
491
+ );
492
+ if (!result.ok || !result.data) {
493
+ console.error(`Error: ${result.error?.message}`);
494
+ process.exit(1);
495
+ }
496
+ if (opts.json) console.log(JSON.stringify(result.data, null, 2));
497
+ else {
498
+ console.log(`\n\u2705 Task created: "${result.data.title}"`);
499
+ console.log(` ID: ${result.data.id}`);
500
+ console.log(` List: ${listName}`);
501
+ if (opts.link) console.log(` \u21B3 Linked to email`);
502
+ console.log('');
503
+ }
504
+ }
505
+ );
506
+
507
+ todoCommand
508
+ .command('update')
509
+ .description('Update a task (title, body, due, importance, status, categories)')
510
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
511
+ .requiredOption('-t, --task <id>', 'Task ID')
512
+ .option('--title <text>', 'New title')
513
+ .option('-b, --body <text>', 'New body/notes')
514
+ .option('-d, --due <ISO-8601>', 'Due date (or omit with --clear-due)')
515
+ .option('--clear-due', 'Remove due date')
516
+ .option('--start <ISO-8601>', 'Start date/time (or omit with --clear-start)')
517
+ .option('--clear-start', 'Remove start date/time')
518
+ .option('--importance <level>', 'Importance: low, normal, high')
519
+ .option('--status <status>', 'Status: notStarted, inProgress, completed, waitingOnOthers, deferred')
520
+ .option('--reminder <ISO-8601>', 'Reminder datetime')
521
+ .option('--clear-reminder', 'Turn off reminder')
522
+ .option('--timezone <tz>', 'Default time zone when setting due/start/reminder', 'UTC')
523
+ .option('--due-tz <tz>', 'Time zone for due only')
524
+ .option('--start-tz <tz>', 'Time zone for start only')
525
+ .option('--reminder-tz <tz>', 'Time zone for reminder only')
526
+ .option(
527
+ '--category <name>',
528
+ 'Set categories to this list (repeatable; replaces existing categories)',
529
+ (v: string, prev: string[]) => [...prev, v],
530
+ [] as string[]
531
+ )
532
+ .option('--clear-categories', 'Remove all categories')
533
+ .option('--recurrence-json <path>', 'JSON file: patternedRecurrence (replaces recurrence)')
534
+ .option('--clear-recurrence', 'Remove recurrence from the task')
535
+ .option('--json', 'Output as JSON')
536
+ .option('--token <token>', 'Use a specific token')
537
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
538
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
539
+ .action(
540
+ async (
541
+ opts: {
542
+ list: string;
543
+ task: string;
544
+ title?: string;
545
+ body?: string;
546
+ due?: string;
547
+ clearDue?: boolean;
548
+ start?: string;
549
+ clearStart?: boolean;
550
+ importance?: string;
551
+ status?: string;
552
+ reminder?: string;
553
+ clearReminder?: boolean;
554
+ category?: string[];
555
+ clearCategories?: boolean;
556
+ recurrenceJson?: string;
557
+ clearRecurrence?: boolean;
558
+ timezone?: string;
559
+ dueTz?: string;
560
+ startTz?: string;
561
+ reminderTz?: string;
562
+ json?: boolean;
563
+ token?: string;
564
+ identity?: string;
565
+ user?: string;
566
+ },
567
+ cmd: any
568
+ ) => {
569
+ checkReadOnly(cmd);
570
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
571
+ if (!auth.success) {
572
+ console.error(`Auth error: ${auth.error}`);
573
+ process.exit(1);
574
+ }
575
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
576
+
577
+ if (opts.clearDue && opts.due !== undefined) {
578
+ console.error('Error: use either --due or --clear-due, not both');
579
+ process.exit(1);
580
+ }
581
+
582
+ if (opts.clearRecurrence && opts.recurrenceJson) {
583
+ console.error('Error: use either --recurrence-json or --clear-recurrence, not both');
584
+ process.exit(1);
585
+ }
586
+ if (opts.clearStart && opts.start !== undefined) {
587
+ console.error('Error: use either --start or --clear-start, not both');
588
+ process.exit(1);
589
+ }
590
+
591
+ const hasField =
592
+ opts.title !== undefined ||
593
+ opts.body !== undefined ||
594
+ opts.due !== undefined ||
595
+ opts.clearDue ||
596
+ opts.start !== undefined ||
597
+ opts.clearStart ||
598
+ opts.importance !== undefined ||
599
+ opts.status !== undefined ||
600
+ opts.reminder !== undefined ||
601
+ opts.clearReminder ||
602
+ (opts.category !== undefined && opts.category.length > 0) ||
603
+ opts.clearCategories ||
604
+ opts.recurrenceJson !== undefined ||
605
+ opts.clearRecurrence;
606
+
607
+ if (!hasField) {
608
+ console.error(
609
+ 'Error: specify at least one of --title, --body, --due, --clear-due, --start, --clear-start, --importance, --status, --reminder, --clear-reminder, --category, --clear-categories, --recurrence-json, --clear-recurrence'
610
+ );
611
+ process.exit(1);
612
+ }
613
+
614
+ if (opts.clearCategories && opts.category !== undefined && opts.category.length > 0) {
615
+ console.error('Error: use either --clear-categories or --category, not both');
616
+ process.exit(1);
617
+ }
618
+
619
+ let importance: TodoImportance | undefined;
620
+ if (opts.importance !== undefined) {
621
+ const valid: TodoImportance[] = ['low', 'normal', 'high'];
622
+ if (!valid.includes(opts.importance as TodoImportance)) {
623
+ console.error(`Invalid importance: ${opts.importance}`);
624
+ process.exit(1);
625
+ }
626
+ importance = opts.importance as TodoImportance;
627
+ }
628
+ let status: TodoStatus | undefined;
629
+ if (opts.status !== undefined) {
630
+ const valid: TodoStatus[] = ['notStarted', 'inProgress', 'completed', 'waitingOnOthers', 'deferred'];
631
+ if (!valid.includes(opts.status as TodoStatus)) {
632
+ console.error(`Invalid status: ${opts.status}`);
633
+ process.exit(1);
634
+ }
635
+ status = opts.status as TodoStatus;
636
+ }
637
+
638
+ const updateOpts: Parameters<typeof updateTask>[3] = {};
639
+ if (opts.title !== undefined) updateOpts.title = opts.title;
640
+ if (opts.body !== undefined) updateOpts.body = opts.body;
641
+ if (opts.clearDue) updateOpts.dueDateTime = null;
642
+ else if (opts.due !== undefined) updateOpts.dueDateTime = opts.due;
643
+ if (opts.clearStart) updateOpts.startDateTime = null;
644
+ else if (opts.start !== undefined) updateOpts.startDateTime = opts.start;
645
+ if (importance !== undefined) updateOpts.importance = importance;
646
+ if (status !== undefined) updateOpts.status = status;
647
+ if (opts.clearReminder) {
648
+ updateOpts.isReminderOn = false;
649
+ updateOpts.reminderDateTime = null;
650
+ } else if (opts.reminder !== undefined) {
651
+ updateOpts.isReminderOn = true;
652
+ updateOpts.reminderDateTime = opts.reminder;
653
+ }
654
+ if (opts.clearCategories) updateOpts.clearCategories = true;
655
+ else if (opts.category !== undefined && opts.category.length > 0) {
656
+ updateOpts.categories = opts.category.map((c) => c.trim()).filter(Boolean);
657
+ }
658
+ if (opts.clearRecurrence) updateOpts.recurrence = null;
659
+ else if (opts.recurrenceJson) {
660
+ const raw = await readFile(opts.recurrenceJson, 'utf-8');
661
+ updateOpts.recurrence = JSON.parse(raw) as Record<string, unknown>;
662
+ }
663
+
664
+ if (opts.due !== undefined || opts.start !== undefined || opts.reminder !== undefined) {
665
+ updateOpts.timeZone = opts.timezone;
666
+ if (opts.dueTz !== undefined) updateOpts.dueTimeZone = opts.dueTz;
667
+ if (opts.startTz !== undefined) updateOpts.startTimeZone = opts.startTz;
668
+ if (opts.reminderTz !== undefined) updateOpts.reminderTimeZone = opts.reminderTz;
669
+ }
670
+
671
+ const r = await updateTask(auth.token!, listId, opts.task, updateOpts, opts.user);
672
+ if (!r.ok || !r.data) {
673
+ console.error(`Error: ${r.error?.message}`);
674
+ process.exit(1);
675
+ }
676
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
677
+ else console.log(`\n\u2705 Updated: "${r.data.title}"\n`);
678
+ }
679
+ );
680
+
681
+ todoCommand
682
+ .command('complete')
683
+ .description('Mark a task as completed')
684
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
685
+ .requiredOption('-t, --task <id>', 'Task ID')
686
+ .option('--json', 'Output as JSON')
687
+ .option('--token <token>', 'Use a specific token')
688
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
689
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
690
+ .action(
691
+ async (
692
+ opts: { list: string; task: string; json?: boolean; token?: string; identity?: string; user?: string },
693
+ cmd: any
694
+ ) => {
695
+ checkReadOnly(cmd);
696
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
697
+ if (!auth.success) {
698
+ console.error(`Auth error: ${auth.error}`);
699
+ process.exit(1);
700
+ }
701
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
702
+ // dateTime should not include Z/offset - keep dateTime and timeZone separate
703
+ const nowISO = new Date().toISOString();
704
+ const now = nowISO.replace('Z', '');
705
+ const r = await updateTask(
706
+ auth.token!,
707
+ listId,
708
+ opts.task,
709
+ { status: 'completed', completedDateTime: now },
710
+ opts.user
711
+ );
712
+ if (!r.ok || !r.data) {
713
+ console.error(`Error: ${r.error?.message}`);
714
+ process.exit(1);
715
+ }
716
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
717
+ else console.log(`\n\u2705 Completed: "${r.data.title}" (${fmtDate(nowISO)})\n`);
718
+ }
719
+ );
720
+
721
+ todoCommand
722
+ .command('delete')
723
+ .description('Delete a task')
724
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
725
+ .requiredOption('-t, --task <id>', 'Task ID')
726
+ .option('--confirm', 'Skip confirmation prompt')
727
+ .option('--token <token>', 'Use a specific token')
728
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
729
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
730
+ .action(
731
+ async (
732
+ opts: { list: string; task: string; confirm?: boolean; token?: string; identity?: string; user?: string },
733
+ cmd: any
734
+ ) => {
735
+ checkReadOnly(cmd);
736
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
737
+ if (!auth.success) {
738
+ console.error(`Auth error: ${auth.error}`);
739
+ process.exit(1);
740
+ }
741
+ const { listId, listDisplay: listName } = await resolveListId(auth.token!, opts.list, opts.user);
742
+ const taskR = await getTask(auth.token!, listId, opts.task, opts.user);
743
+ if (!taskR.ok || !taskR.data) {
744
+ console.error(`Task not found: ${taskR.error?.message}`);
745
+ process.exit(1);
746
+ }
747
+ if (!opts.confirm) {
748
+ console.log(`Delete "${taskR.data.title}" from "${listName}"? (ID: ${opts.task})`);
749
+ console.log('Run with --confirm to confirm.');
750
+ process.exit(1);
751
+ }
752
+ const r = await deleteTask(auth.token!, listId, opts.task, opts.user);
753
+ if (!r.ok) {
754
+ console.error(`Error: ${r.error?.message}`);
755
+ process.exit(1);
756
+ }
757
+ console.log(`\n\u{1F5D1} Deleted: "${taskR.data.title}"\n`);
758
+ }
759
+ );
760
+
761
+ todoCommand
762
+ .command('add-checklist')
763
+ .description('Add a checklist (subtask) item to a task')
764
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
765
+ .requiredOption('-t, --task <id>', 'Task ID')
766
+ .requiredOption('-n, --name <text>', 'Checklist item text')
767
+ .option('--json', 'Output as JSON')
768
+ .option('--token <token>', 'Use a specific token')
769
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
770
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
771
+ .action(
772
+ async (
773
+ opts: {
774
+ list: string;
775
+ task: string;
776
+ name: string;
777
+ json?: boolean;
778
+ token?: string;
779
+ identity?: string;
780
+ user?: string;
781
+ },
782
+ cmd: any
783
+ ) => {
784
+ checkReadOnly(cmd);
785
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
786
+ if (!auth.success) {
787
+ console.error(`Auth error: ${auth.error}`);
788
+ process.exit(1);
789
+ }
790
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
791
+ const r = await addChecklistItem(auth.token!, listId, opts.task, opts.name, opts.user);
792
+ if (!r.ok || !r.data) {
793
+ console.error(`Error: ${r.error?.message}`);
794
+ process.exit(1);
795
+ }
796
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
797
+ else console.log(`\n\u2705 Added: "${r.data.displayName}" (${r.data.id})\n`);
798
+ }
799
+ );
800
+
801
+ todoCommand
802
+ .command('update-checklist')
803
+ .description('Update a checklist item (rename or check/uncheck)')
804
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
805
+ .requiredOption('-t, --task <id>', 'Task ID')
806
+ .requiredOption('-c, --item <checklistItemId>', 'Checklist item ID')
807
+ .option('-n, --name <text>', 'New display text')
808
+ .option('--checked', 'Mark checked')
809
+ .option('--unchecked', 'Mark unchecked')
810
+ .option('--json', 'Output as JSON')
811
+ .option('--token <token>', 'Use a specific token')
812
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
813
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
814
+ .action(
815
+ async (
816
+ opts: {
817
+ list: string;
818
+ task: string;
819
+ item: string;
820
+ name?: string;
821
+ checked?: boolean;
822
+ unchecked?: boolean;
823
+ json?: boolean;
824
+ token?: string;
825
+ identity?: string;
826
+ user?: string;
827
+ },
828
+ cmd: any
829
+ ) => {
830
+ checkReadOnly(cmd);
831
+ if (opts.checked && opts.unchecked) {
832
+ console.error('Error: use either --checked or --unchecked, not both');
833
+ process.exit(1);
834
+ }
835
+ if (opts.name === undefined && !opts.checked && !opts.unchecked) {
836
+ console.error('Error: specify --name and/or --checked/--unchecked');
837
+ process.exit(1);
838
+ }
839
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
840
+ if (!auth.success) {
841
+ console.error(`Auth error: ${auth.error}`);
842
+ process.exit(1);
843
+ }
844
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
845
+ const patch: { displayName?: string; isChecked?: boolean } = {};
846
+ if (opts.name !== undefined) patch.displayName = opts.name;
847
+ if (opts.checked) patch.isChecked = true;
848
+ if (opts.unchecked) patch.isChecked = false;
849
+ const r = await updateChecklistItem(auth.token!, listId, opts.task, opts.item, patch, opts.user);
850
+ if (!r.ok || !r.data) {
851
+ console.error(`Error: ${r.error?.message}`);
852
+ process.exit(1);
853
+ }
854
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
855
+ else console.log(`\n\u2705 Updated checklist item: "${r.data.displayName}"\n`);
856
+ }
857
+ );
858
+
859
+ todoCommand
860
+ .command('delete-checklist')
861
+ .description('Delete a checklist item from a task')
862
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
863
+ .requiredOption('-t, --task <id>', 'Task ID')
864
+ .requiredOption('-c, --item <checklistItemId>', 'Checklist item ID')
865
+ .option('--confirm', 'Confirm without prompt')
866
+ .option('--token <token>', 'Use a specific token')
867
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
868
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
869
+ .action(
870
+ async (
871
+ opts: {
872
+ list: string;
873
+ task: string;
874
+ item: string;
875
+ confirm?: boolean;
876
+ token?: string;
877
+ identity?: string;
878
+ user?: string;
879
+ },
880
+ cmd: any
881
+ ) => {
882
+ checkReadOnly(cmd);
883
+ if (!opts.confirm) {
884
+ console.log(`Delete checklist item ${opts.item}? Run with --confirm.`);
885
+ process.exit(1);
886
+ }
887
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
888
+ if (!auth.success) {
889
+ console.error(`Auth error: ${auth.error}`);
890
+ process.exit(1);
891
+ }
892
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
893
+ const r = await deleteChecklistItem(auth.token!, listId, opts.task, opts.item, opts.user);
894
+ if (!r.ok) {
895
+ console.error(`Error: ${r.error?.message}`);
896
+ process.exit(1);
897
+ }
898
+ console.log(`\n\u2705 Deleted checklist item: ${opts.item}\n`);
899
+ }
900
+ );
901
+
902
+ todoCommand
903
+ .command('create-list')
904
+ .description('Create a new To Do list')
905
+ .requiredOption('-n, --name <displayName>', 'List display name')
906
+ .option('--json', 'Output as JSON')
907
+ .option('--token <token>', 'Use a specific token')
908
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
909
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
910
+ .action(
911
+ async (opts: { name: string; json?: boolean; token?: string; identity?: string; user?: string }, cmd: any) => {
912
+ checkReadOnly(cmd);
913
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
914
+ if (!auth.success) {
915
+ console.error(`Auth error: ${auth.error}`);
916
+ process.exit(1);
917
+ }
918
+ const r = await createTodoList(auth.token!, opts.name, opts.user);
919
+ if (!r.ok || !r.data) {
920
+ console.error(`Error: ${r.error?.message}`);
921
+ process.exit(1);
922
+ }
923
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
924
+ else console.log(`\n\u2705 Created list: "${r.data.displayName}" (${r.data.id})\n`);
925
+ }
926
+ );
927
+
928
+ todoCommand
929
+ .command('update-list')
930
+ .description('Rename a To Do list')
931
+ .requiredOption('-l, --list <name|id>', 'Current list name or ID')
932
+ .requiredOption('-n, --name <displayName>', 'New display name')
933
+ .option('--json', 'Output as JSON')
934
+ .option('--token <token>', 'Use a specific token')
935
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
936
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
937
+ .action(
938
+ async (
939
+ opts: { list: string; name: string; json?: boolean; token?: string; identity?: string; user?: string },
940
+ cmd: any
941
+ ) => {
942
+ checkReadOnly(cmd);
943
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
944
+ if (!auth.success) {
945
+ console.error(`Auth error: ${auth.error}`);
946
+ process.exit(1);
947
+ }
948
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
949
+ const r = await updateTodoList(auth.token!, listId, opts.name, opts.user);
950
+ if (!r.ok || !r.data) {
951
+ console.error(`Error: ${r.error?.message}`);
952
+ process.exit(1);
953
+ }
954
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
955
+ else console.log(`\n\u2705 Renamed list to: "${r.data.displayName}"\n`);
956
+ }
957
+ );
958
+
959
+ todoCommand
960
+ .command('delete-list')
961
+ .description('Delete a To Do list')
962
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
963
+ .option('--confirm', 'Confirm deletion')
964
+ .option('--json', 'Output as JSON')
965
+ .option('--token <token>', 'Use a specific token')
966
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
967
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
968
+ .action(
969
+ async (
970
+ opts: { list: string; confirm?: boolean; json?: boolean; token?: string; identity?: string; user?: string },
971
+ cmd: any
972
+ ) => {
973
+ checkReadOnly(cmd);
974
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
975
+ if (!auth.success) {
976
+ console.error(`Auth error: ${auth.error}`);
977
+ process.exit(1);
978
+ }
979
+ const { listId, listDisplay } = await resolveListId(auth.token!, opts.list, opts.user);
980
+ if (!opts.confirm) {
981
+ console.log(`Delete list "${listDisplay}"? (ID: ${listId})`);
982
+ console.log('Run with --confirm to confirm.');
983
+ process.exit(1);
984
+ }
985
+ const r = await deleteTodoList(auth.token!, listId, opts.user);
986
+ if (!r.ok) {
987
+ console.error(`Error: ${r.error?.message}`);
988
+ process.exit(1);
989
+ }
990
+ if (opts.json) console.log(JSON.stringify({ deleted: listId }, null, 2));
991
+ else console.log(`\n\u2705 Deleted list: "${listDisplay}"\n`);
992
+ }
993
+ );
994
+
995
+ todoCommand
996
+ .command('list-attachments')
997
+ .description('List attachments on a task')
998
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
999
+ .requiredOption('-t, --task <id>', 'Task ID')
1000
+ .option('--json', 'Output as JSON')
1001
+ .option('--token <token>', 'Use a specific token')
1002
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1003
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1004
+ .action(
1005
+ async (opts: { list: string; task: string; json?: boolean; token?: string; identity?: string; user?: string }) => {
1006
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1007
+ if (!auth.success) {
1008
+ console.error(`Auth error: ${auth.error}`);
1009
+ process.exit(1);
1010
+ }
1011
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1012
+ const r = await listAttachments(auth.token!, listId, opts.task, opts.user);
1013
+ if (!r.ok || !r.data) {
1014
+ console.error(`Error: ${r.error?.message}`);
1015
+ process.exit(1);
1016
+ }
1017
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1018
+ else {
1019
+ for (const a of r.data) {
1020
+ console.log(`- ${a.name || a.id} (${a.id})${a.size != null ? ` ${a.size} bytes` : ''}`);
1021
+ }
1022
+ }
1023
+ }
1024
+ );
1025
+
1026
+ todoCommand
1027
+ .command('add-attachment')
1028
+ .description('Attach a small file to a task (base64 upload; Graph size limits apply)')
1029
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1030
+ .requiredOption('-t, --task <id>', 'Task ID')
1031
+ .requiredOption('-f, --file <path>', 'Local file path')
1032
+ .option('--name <filename>', 'Attachment name (default: file basename)')
1033
+ .option('--content-type <mime>', 'MIME type (default: application/octet-stream)')
1034
+ .option('--json', 'Output as JSON')
1035
+ .option('--token <token>', 'Use a specific token')
1036
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1037
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1038
+ .action(
1039
+ async (
1040
+ opts: {
1041
+ list: string;
1042
+ task: string;
1043
+ file: string;
1044
+ name?: string;
1045
+ contentType?: string;
1046
+ json?: boolean;
1047
+ token?: string;
1048
+ identity?: string;
1049
+ user?: string;
1050
+ },
1051
+ cmd: any
1052
+ ) => {
1053
+ checkReadOnly(cmd);
1054
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1055
+ if (!auth.success) {
1056
+ console.error(`Auth error: ${auth.error}`);
1057
+ process.exit(1);
1058
+ }
1059
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1060
+ const buf = await readFile(opts.file);
1061
+ const b64 = buf.toString('base64');
1062
+ const attName = opts.name?.trim() || basename(opts.file);
1063
+ const ct = opts.contentType?.trim() || 'application/octet-stream';
1064
+ const r = await createTaskFileAttachment(auth.token!, listId, opts.task, attName, b64, ct, opts.user);
1065
+ if (!r.ok || !r.data) {
1066
+ console.error(`Error: ${r.error?.message}`);
1067
+ process.exit(1);
1068
+ }
1069
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1070
+ else console.log(`\n\u2705 Attached: ${r.data.name || r.data.id} (${r.data.id})\n`);
1071
+ }
1072
+ );
1073
+
1074
+ todoCommand
1075
+ .command('delete-attachment')
1076
+ .description('Remove an attachment from a task')
1077
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1078
+ .requiredOption('-t, --task <id>', 'Task ID')
1079
+ .requiredOption('-a, --attachment <attachmentId>', 'Attachment ID')
1080
+ .option('--confirm', 'Confirm without prompt')
1081
+ .option('--token <token>', 'Use a specific token')
1082
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1083
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1084
+ .action(
1085
+ async (
1086
+ opts: {
1087
+ list: string;
1088
+ task: string;
1089
+ attachment: string;
1090
+ confirm?: boolean;
1091
+ token?: string;
1092
+ identity?: string;
1093
+ user?: string;
1094
+ },
1095
+ cmd: any
1096
+ ) => {
1097
+ checkReadOnly(cmd);
1098
+ if (!opts.confirm) {
1099
+ console.log(`Delete attachment ${opts.attachment}? Run with --confirm.`);
1100
+ process.exit(1);
1101
+ }
1102
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1103
+ if (!auth.success) {
1104
+ console.error(`Auth error: ${auth.error}`);
1105
+ process.exit(1);
1106
+ }
1107
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1108
+ const r = await deleteAttachment(auth.token!, listId, opts.task, opts.attachment, opts.user);
1109
+ if (!r.ok) {
1110
+ console.error(`Error: ${r.error?.message}`);
1111
+ process.exit(1);
1112
+ }
1113
+ console.log(`\n\u2705 Deleted attachment: ${opts.attachment}\n`);
1114
+ }
1115
+ );
1116
+
1117
+ todoCommand
1118
+ .command('get-attachment')
1119
+ .description('Fetch metadata for one task attachment')
1120
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1121
+ .requiredOption('-t, --task <id>', 'Task ID')
1122
+ .requiredOption('-a, --attachment <attachmentId>', 'Attachment ID')
1123
+ .option('--json', 'Output as JSON')
1124
+ .option('--token <token>', 'Use a specific token')
1125
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1126
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1127
+ .action(
1128
+ async (opts: {
1129
+ list: string;
1130
+ task: string;
1131
+ attachment: string;
1132
+ json?: boolean;
1133
+ token?: string;
1134
+ identity?: string;
1135
+ user?: string;
1136
+ }) => {
1137
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1138
+ if (!auth.success) {
1139
+ console.error(`Auth error: ${auth.error}`);
1140
+ process.exit(1);
1141
+ }
1142
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1143
+ const r = await getTaskAttachment(auth.token!, listId, opts.task, opts.attachment, opts.user);
1144
+ if (!r.ok || !r.data) {
1145
+ console.error(`Error: ${r.error?.message}`);
1146
+ process.exit(1);
1147
+ }
1148
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1149
+ else {
1150
+ const a = r.data;
1151
+ console.log(`Name: ${a.name || '(unnamed)'}`);
1152
+ console.log(`ID: ${a.id}`);
1153
+ if (a.contentType) console.log(`Type: ${a.contentType}`);
1154
+ if (a.size != null) console.log(`Size: ${a.size}`);
1155
+ }
1156
+ }
1157
+ );
1158
+
1159
+ todoCommand
1160
+ .command('download-attachment')
1161
+ .description(
1162
+ 'Download file attachment bytes (Graph GET .../attachments/{id}/$value); not for reference/URL attachments'
1163
+ )
1164
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1165
+ .requiredOption('-t, --task <id>', 'Task ID')
1166
+ .requiredOption('-a, --attachment <attachmentId>', 'Attachment ID')
1167
+ .requiredOption('-o, --output <path>', 'Write file to this path')
1168
+ .option('--token <token>', 'Use a specific token')
1169
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1170
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1171
+ .action(
1172
+ async (opts: {
1173
+ list: string;
1174
+ task: string;
1175
+ attachment: string;
1176
+ output: string;
1177
+ token?: string;
1178
+ identity?: string;
1179
+ user?: string;
1180
+ }) => {
1181
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1182
+ if (!auth.success) {
1183
+ console.error(`Auth error: ${auth.error}`);
1184
+ process.exit(1);
1185
+ }
1186
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1187
+ const r = await getTaskAttachmentContent(auth.token!, listId, opts.task, opts.attachment, opts.user);
1188
+ if (!r.ok || !r.data) {
1189
+ console.error(`Error: ${r.error?.message}`);
1190
+ process.exit(1);
1191
+ }
1192
+ await writeFile(opts.output, r.data);
1193
+ console.log(`Wrote ${r.data.byteLength} bytes to ${opts.output}`);
1194
+ }
1195
+ );
1196
+
1197
+ todoCommand
1198
+ .command('add-reference-attachment')
1199
+ .description('Add a URL reference attachment (not file bytes)')
1200
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1201
+ .requiredOption('-t, --task <id>', 'Task ID')
1202
+ .requiredOption('--url <url>', 'Target URL')
1203
+ .requiredOption('-n, --name <text>', 'Attachment display name')
1204
+ .option('--json', 'Output as JSON')
1205
+ .option('--token <token>', 'Use a specific token')
1206
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1207
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1208
+ .action(
1209
+ async (
1210
+ opts: {
1211
+ list: string;
1212
+ task: string;
1213
+ url: string;
1214
+ name: string;
1215
+ json?: boolean;
1216
+ token?: string;
1217
+ identity?: string;
1218
+ user?: string;
1219
+ },
1220
+ cmd: any
1221
+ ) => {
1222
+ checkReadOnly(cmd);
1223
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1224
+ if (!auth.success) {
1225
+ console.error(`Auth error: ${auth.error}`);
1226
+ process.exit(1);
1227
+ }
1228
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1229
+ const r = await createTaskReferenceAttachment(auth.token!, listId, opts.task, opts.name, opts.url, opts.user);
1230
+ if (!r.ok || !r.data) {
1231
+ console.error(`Error: ${r.error?.message}`);
1232
+ process.exit(1);
1233
+ }
1234
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1235
+ else console.log(`\n\u2705 Reference attachment: ${r.data.name || r.data.id} (${r.data.id})\n`);
1236
+ }
1237
+ );
1238
+
1239
+ todoCommand
1240
+ .command('add-linked-resource')
1241
+ .description('Merge linked resources on the task (PATCH task). Prefer todo linked-resource create for Graph POST.')
1242
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1243
+ .requiredOption('-t, --task <id>', 'Task ID')
1244
+ .requiredOption('--url <url>', 'Resource webUrl')
1245
+ .option('-d, --description <text>', 'Title (Graph displayName; legacy alias)')
1246
+ .option('--display-name <text>', 'Graph displayName (same as -d)')
1247
+ .option('--icon <url>', 'Optional icon URL')
1248
+ .option('--json', 'Output as JSON')
1249
+ .option('--token <token>', 'Use a specific token')
1250
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1251
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1252
+ .action(
1253
+ async (
1254
+ opts: {
1255
+ list: string;
1256
+ task: string;
1257
+ url: string;
1258
+ description?: string;
1259
+ displayName?: string;
1260
+ icon?: string;
1261
+ json?: boolean;
1262
+ token?: string;
1263
+ identity?: string;
1264
+ user?: string;
1265
+ },
1266
+ cmd: any
1267
+ ) => {
1268
+ checkReadOnly(cmd);
1269
+ const title = opts.displayName?.trim() || opts.description?.trim();
1270
+ if (!title) {
1271
+ console.error('Error: specify --display-name or -d/--description (Graph displayName)');
1272
+ process.exit(1);
1273
+ }
1274
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1275
+ if (!auth.success) {
1276
+ console.error(`Auth error: ${auth.error}`);
1277
+ process.exit(1);
1278
+ }
1279
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1280
+ const r = await addLinkedResource(
1281
+ auth.token!,
1282
+ listId,
1283
+ opts.task,
1284
+ { webUrl: opts.url, displayName: title, iconUrl: opts.icon },
1285
+ opts.user
1286
+ );
1287
+ if (!r.ok || !r.data) {
1288
+ console.error(`Error: ${r.error?.message}`);
1289
+ process.exit(1);
1290
+ }
1291
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1292
+ else console.log(`\n\u2705 Linked resource added. Task: "${r.data.title}"\n`);
1293
+ }
1294
+ );
1295
+
1296
+ todoCommand
1297
+ .command('remove-linked-resource')
1298
+ .description('Remove a linked resource by matching webUrl')
1299
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1300
+ .requiredOption('-t, --task <id>', 'Task ID')
1301
+ .requiredOption('--url <url>', 'webUrl to remove')
1302
+ .option('--json', 'Output as JSON')
1303
+ .option('--token <token>', 'Use a specific token')
1304
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1305
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1306
+ .action(
1307
+ async (
1308
+ opts: {
1309
+ list: string;
1310
+ task: string;
1311
+ url: string;
1312
+ json?: boolean;
1313
+ token?: string;
1314
+ identity?: string;
1315
+ user?: string;
1316
+ },
1317
+ cmd: any
1318
+ ) => {
1319
+ checkReadOnly(cmd);
1320
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1321
+ if (!auth.success) {
1322
+ console.error(`Auth error: ${auth.error}`);
1323
+ process.exit(1);
1324
+ }
1325
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1326
+ const r = await removeLinkedResourceByWebUrl(auth.token!, listId, opts.task, opts.url, opts.user);
1327
+ if (!r.ok || !r.data) {
1328
+ console.error(`Error: ${r.error?.message}`);
1329
+ process.exit(1);
1330
+ }
1331
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1332
+ else console.log(`\n\u2705 Removed linked resource matching URL.\n`);
1333
+ }
1334
+ );
1335
+
1336
+ todoCommand
1337
+ .command('upload-attachment-large')
1338
+ .description('Upload a large file via Graph upload session (chunked PUT)')
1339
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1340
+ .requiredOption('-t, --task <id>', 'Task ID')
1341
+ .requiredOption('-f, --file <path>', 'Local file path')
1342
+ .option('-n, --name <filename>', 'Attachment name (default: file basename)')
1343
+ .option('--json', 'Output as JSON')
1344
+ .option('--token <token>', 'Use a specific token')
1345
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1346
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1347
+ .action(
1348
+ async (
1349
+ opts: {
1350
+ list: string;
1351
+ task: string;
1352
+ file: string;
1353
+ name?: string;
1354
+ json?: boolean;
1355
+ token?: string;
1356
+ identity?: string;
1357
+ user?: string;
1358
+ },
1359
+ cmd: any
1360
+ ) => {
1361
+ checkReadOnly(cmd);
1362
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1363
+ if (!auth.success) {
1364
+ console.error(`Auth error: ${auth.error}`);
1365
+ process.exit(1);
1366
+ }
1367
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1368
+ const r = await uploadLargeFileAttachment(auth.token!, listId, opts.task, opts.file, opts.name, opts.user);
1369
+ if (!r.ok || !r.data) {
1370
+ console.error(`Error: ${r.error?.message}`);
1371
+ process.exit(1);
1372
+ }
1373
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1374
+ else console.log(`\n\u2705 Large attachment: ${r.data.name || r.data.id} (${r.data.id})\n`);
1375
+ }
1376
+ );
1377
+
1378
+ todoCommand
1379
+ .command('delta')
1380
+ .description('One page of todo task delta (use -l for first page, or --url for nextLink/deltaLink)')
1381
+ .option('-l, --list <name|id>', 'List name or ID (first page only)')
1382
+ .option('--url <fullUrl>', 'Full nextLink or deltaLink URL from a previous response')
1383
+ .option('--token <token>', 'Use a specific token')
1384
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1385
+ .option('--user <email>', 'Target user (first page only; --url encodes scope)')
1386
+ .action(async (opts: { list?: string; url?: string; token?: string; identity?: string; user?: string }) => {
1387
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1388
+ if (!auth.success) {
1389
+ console.error(`Auth error: ${auth.error}`);
1390
+ process.exit(1);
1391
+ }
1392
+ const r = opts.url
1393
+ ? await getTodoTasksDeltaPage(auth.token!, '', opts.url)
1394
+ : await (async () => {
1395
+ if (!opts.list) {
1396
+ console.error('Error: specify --list for the first delta page, or --url to follow nextLink/deltaLink');
1397
+ process.exit(1);
1398
+ }
1399
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1400
+ return getTodoTasksDeltaPage(auth.token!, listId, undefined, opts.user);
1401
+ })();
1402
+ if (!r.ok || !r.data) {
1403
+ console.error(`Error: ${r.error?.message}`);
1404
+ process.exit(1);
1405
+ }
1406
+ console.log(JSON.stringify(r.data, null, 2));
1407
+ });
1408
+
1409
+ todoCommand
1410
+ .command('list-checklist-items')
1411
+ .description('List checklist items via GET collection (same items as on the task object)')
1412
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1413
+ .requiredOption('-t, --task <id>', 'Task ID')
1414
+ .option('--json', 'Output as JSON')
1415
+ .option('--token <token>', 'Use a specific token')
1416
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1417
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1418
+ .action(
1419
+ async (opts: { list: string; task: string; json?: boolean; token?: string; identity?: string; user?: string }) => {
1420
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1421
+ if (!auth.success) {
1422
+ console.error(`Auth error: ${auth.error}`);
1423
+ process.exit(1);
1424
+ }
1425
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1426
+ const r = await listTaskChecklistItems(auth.token!, listId, opts.task, opts.user);
1427
+ if (!r.ok || !r.data) {
1428
+ console.error(`Error: ${r.error?.message}`);
1429
+ process.exit(1);
1430
+ }
1431
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1432
+ else {
1433
+ for (const it of r.data) {
1434
+ console.log(`${it.isChecked ? '\u2611' : '\u2610'} ${it.displayName} (${it.id})`);
1435
+ }
1436
+ }
1437
+ }
1438
+ );
1439
+
1440
+ todoCommand
1441
+ .command('get-checklist-item')
1442
+ .description('Get one checklist item by id (Graph GET checklistItems/{id})')
1443
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1444
+ .requiredOption('-t, --task <id>', 'Task ID')
1445
+ .requiredOption('-c, --checklist-item <id>', 'Checklist item id')
1446
+ .option('--json', 'Output as JSON')
1447
+ .option('--token <token>', 'Use a specific token')
1448
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1449
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1450
+ .action(
1451
+ async (opts: {
1452
+ list: string;
1453
+ task: string;
1454
+ checklistItem: string;
1455
+ json?: boolean;
1456
+ token?: string;
1457
+ identity?: string;
1458
+ user?: string;
1459
+ }) => {
1460
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1461
+ if (!auth.success) {
1462
+ console.error(`Auth error: ${auth.error}`);
1463
+ process.exit(1);
1464
+ }
1465
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1466
+ const r = await getChecklistItem(auth.token!, listId, opts.task, opts.checklistItem, opts.user);
1467
+ if (!r.ok || !r.data) {
1468
+ console.error(`Error: ${r.error?.message}`);
1469
+ process.exit(1);
1470
+ }
1471
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1472
+ else {
1473
+ const it = r.data;
1474
+ console.log(`${it.isChecked ? '\u2611' : '\u2610'} ${it.displayName}`);
1475
+ console.log(`ID: ${it.id}`);
1476
+ if (it.createdDateTime) console.log(`Created: ${it.createdDateTime}`);
1477
+ if (it.checkedDateTime) console.log(`Checked: ${it.checkedDateTime}`);
1478
+ }
1479
+ }
1480
+ );
1481
+
1482
+ const todoLinkedResourceCommand = new Command('linked-resource').description(
1483
+ 'Graph linkedResource endpoints (per-item REST)'
1484
+ );
1485
+
1486
+ todoLinkedResourceCommand
1487
+ .command('list')
1488
+ .description('List linked resources for a task')
1489
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1490
+ .requiredOption('-t, --task <id>', 'Task ID')
1491
+ .option('--json', 'Output as JSON')
1492
+ .option('--token <token>', 'Use a specific token')
1493
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1494
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1495
+ .action(
1496
+ async (opts: { list: string; task: string; json?: boolean; token?: string; identity?: string; user?: string }) => {
1497
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1498
+ if (!auth.success) {
1499
+ console.error(`Auth error: ${auth.error}`);
1500
+ process.exit(1);
1501
+ }
1502
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1503
+ const r = await listTaskLinkedResources(auth.token!, listId, opts.task, opts.user);
1504
+ if (!r.ok || !r.data) {
1505
+ console.error(`Error: ${r.error?.message}`);
1506
+ process.exit(1);
1507
+ }
1508
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1509
+ else {
1510
+ for (const lr of r.data) {
1511
+ console.log(`- ${linkedTitle(lr)} ${lr.webUrl ?? ''} (${lr.id})`);
1512
+ }
1513
+ }
1514
+ }
1515
+ );
1516
+
1517
+ todoLinkedResourceCommand
1518
+ .command('create')
1519
+ .description('POST a linkedResource (Graph native create)')
1520
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1521
+ .requiredOption('-t, --task <id>', 'Task ID')
1522
+ .option('--url <url>', 'webUrl')
1523
+ .requiredOption('-n, --name <text>', 'displayName')
1524
+ .option('--application-name <text>', 'applicationName')
1525
+ .option('--external-id <id>', 'externalId')
1526
+ .option('--json', 'Output as JSON')
1527
+ .option('--token <token>', 'Use a specific token')
1528
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1529
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1530
+ .action(
1531
+ async (
1532
+ opts: {
1533
+ list: string;
1534
+ task: string;
1535
+ url?: string;
1536
+ name: string;
1537
+ applicationName?: string;
1538
+ externalId?: string;
1539
+ json?: boolean;
1540
+ token?: string;
1541
+ identity?: string;
1542
+ user?: string;
1543
+ },
1544
+ cmd: any
1545
+ ) => {
1546
+ checkReadOnly(cmd);
1547
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1548
+ if (!auth.success) {
1549
+ console.error(`Auth error: ${auth.error}`);
1550
+ process.exit(1);
1551
+ }
1552
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1553
+ const r = await createTaskLinkedResource(
1554
+ auth.token!,
1555
+ listId,
1556
+ opts.task,
1557
+ {
1558
+ webUrl: opts.url,
1559
+ displayName: opts.name,
1560
+ applicationName: opts.applicationName,
1561
+ externalId: opts.externalId
1562
+ },
1563
+ opts.user
1564
+ );
1565
+ if (!r.ok || !r.data) {
1566
+ console.error(`Error: ${r.error?.message}`);
1567
+ process.exit(1);
1568
+ }
1569
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1570
+ else console.log(`\n\u2705 Linked resource: ${linkedTitle(r.data)} (${r.data.id})\n`);
1571
+ }
1572
+ );
1573
+
1574
+ todoLinkedResourceCommand
1575
+ .command('get')
1576
+ .description('GET one linked resource by id')
1577
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1578
+ .requiredOption('-t, --task <id>', 'Task ID')
1579
+ .requiredOption('-i, --id <linkedResourceId>', 'linkedResource id')
1580
+ .option('--json', 'Output as JSON')
1581
+ .option('--token <token>', 'Use a specific token')
1582
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1583
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1584
+ .action(
1585
+ async (opts: {
1586
+ list: string;
1587
+ task: string;
1588
+ id: string;
1589
+ json?: boolean;
1590
+ token?: string;
1591
+ identity?: string;
1592
+ user?: string;
1593
+ }) => {
1594
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1595
+ if (!auth.success) {
1596
+ console.error(`Auth error: ${auth.error}`);
1597
+ process.exit(1);
1598
+ }
1599
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1600
+ const r = await getTaskLinkedResource(auth.token!, listId, opts.task, opts.id, opts.user);
1601
+ if (!r.ok || !r.data) {
1602
+ console.error(`Error: ${r.error?.message}`);
1603
+ process.exit(1);
1604
+ }
1605
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1606
+ else console.log(JSON.stringify(r.data, null, 2));
1607
+ }
1608
+ );
1609
+
1610
+ todoLinkedResourceCommand
1611
+ .command('update')
1612
+ .description('PATCH a linked resource')
1613
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1614
+ .requiredOption('-t, --task <id>', 'Task ID')
1615
+ .requiredOption('-i, --id <linkedResourceId>', 'linkedResource id')
1616
+ .option('--url <url>', 'webUrl')
1617
+ .option('-n, --name <text>', 'displayName')
1618
+ .option('--application-name <text>', 'applicationName')
1619
+ .option('--external-id <id>', 'externalId')
1620
+ .option('--json', 'Output as JSON')
1621
+ .option('--token <token>', 'Use a specific token')
1622
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1623
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1624
+ .action(
1625
+ async (
1626
+ opts: {
1627
+ list: string;
1628
+ task: string;
1629
+ id: string;
1630
+ url?: string;
1631
+ name?: string;
1632
+ applicationName?: string;
1633
+ externalId?: string;
1634
+ json?: boolean;
1635
+ token?: string;
1636
+ identity?: string;
1637
+ user?: string;
1638
+ },
1639
+ cmd: any
1640
+ ) => {
1641
+ checkReadOnly(cmd);
1642
+ if (
1643
+ opts.url === undefined &&
1644
+ opts.name === undefined &&
1645
+ opts.applicationName === undefined &&
1646
+ opts.externalId === undefined
1647
+ ) {
1648
+ console.error('Error: specify at least one of --url, --name, --application-name, --external-id');
1649
+ process.exit(1);
1650
+ }
1651
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1652
+ if (!auth.success) {
1653
+ console.error(`Auth error: ${auth.error}`);
1654
+ process.exit(1);
1655
+ }
1656
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1657
+ const r = await updateTaskLinkedResource(
1658
+ auth.token!,
1659
+ listId,
1660
+ opts.task,
1661
+ opts.id,
1662
+ {
1663
+ webUrl: opts.url,
1664
+ displayName: opts.name,
1665
+ applicationName: opts.applicationName,
1666
+ externalId: opts.externalId
1667
+ },
1668
+ opts.user
1669
+ );
1670
+ if (!r.ok || !r.data) {
1671
+ console.error(`Error: ${r.error?.message}`);
1672
+ process.exit(1);
1673
+ }
1674
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1675
+ else console.log(`\n\u2705 Updated linked resource ${opts.id}\n`);
1676
+ }
1677
+ );
1678
+
1679
+ todoLinkedResourceCommand
1680
+ .command('delete')
1681
+ .description('DELETE a linked resource by id')
1682
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1683
+ .requiredOption('-t, --task <id>', 'Task ID')
1684
+ .requiredOption('-i, --id <linkedResourceId>', 'linkedResource id')
1685
+ .option('--confirm', 'Confirm without prompt')
1686
+ .option('--token <token>', 'Use a specific token')
1687
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1688
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1689
+ .action(
1690
+ async (
1691
+ opts: {
1692
+ list: string;
1693
+ task: string;
1694
+ id: string;
1695
+ confirm?: boolean;
1696
+ token?: string;
1697
+ identity?: string;
1698
+ user?: string;
1699
+ },
1700
+ cmd: any
1701
+ ) => {
1702
+ checkReadOnly(cmd);
1703
+ if (!opts.confirm) {
1704
+ console.log(`Delete linked resource ${opts.id}? Run with --confirm.`);
1705
+ process.exit(1);
1706
+ }
1707
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1708
+ if (!auth.success) {
1709
+ console.error(`Auth error: ${auth.error}`);
1710
+ process.exit(1);
1711
+ }
1712
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1713
+ const r = await deleteTaskLinkedResource(auth.token!, listId, opts.task, opts.id, opts.user);
1714
+ if (!r.ok) {
1715
+ console.error(`Error: ${r.error?.message}`);
1716
+ process.exit(1);
1717
+ }
1718
+ console.log(`\n\u2705 Deleted linked resource ${opts.id}\n`);
1719
+ }
1720
+ );
1721
+
1722
+ todoCommand.addCommand(todoLinkedResourceCommand);
1723
+
1724
+ const todoListExtensionCommand = new Command('list-extension').description(
1725
+ 'Open type extensions on a task list (Graph)'
1726
+ );
1727
+
1728
+ todoListExtensionCommand
1729
+ .command('list')
1730
+ .description('List open extensions on a task list')
1731
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1732
+ .option('--json', 'Output as JSON')
1733
+ .option('--token <token>', 'Use a specific token')
1734
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1735
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1736
+ .action(async (opts: { list: string; json?: boolean; token?: string; identity?: string; user?: string }) => {
1737
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1738
+ if (!auth.success) {
1739
+ console.error(`Auth error: ${auth.error}`);
1740
+ process.exit(1);
1741
+ }
1742
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1743
+ const r = await listTodoListOpenExtensions(auth.token!, listId, opts.user);
1744
+ if (!r.ok || !r.data) {
1745
+ console.error(`Error: ${r.error?.message}`);
1746
+ process.exit(1);
1747
+ }
1748
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1749
+ else {
1750
+ for (const ext of r.data) {
1751
+ const name = (ext.extensionName as string) || JSON.stringify(ext);
1752
+ console.log(`- ${name}`);
1753
+ }
1754
+ }
1755
+ });
1756
+
1757
+ todoListExtensionCommand
1758
+ .command('get')
1759
+ .description('Get one open extension on a list')
1760
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1761
+ .requiredOption('-n, --name <id>', 'extensionName')
1762
+ .option('--json', 'Output as JSON')
1763
+ .option('--token <token>', 'Use a specific token')
1764
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1765
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1766
+ .action(
1767
+ async (opts: { list: string; name: string; json?: boolean; token?: string; identity?: string; user?: string }) => {
1768
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1769
+ if (!auth.success) {
1770
+ console.error(`Auth error: ${auth.error}`);
1771
+ process.exit(1);
1772
+ }
1773
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1774
+ const r = await getTodoListOpenExtension(auth.token!, listId, opts.name, opts.user);
1775
+ if (!r.ok || !r.data) {
1776
+ console.error(`Error: ${r.error?.message}`);
1777
+ process.exit(1);
1778
+ }
1779
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1780
+ else console.log(JSON.stringify(r.data, null, 2));
1781
+ }
1782
+ );
1783
+
1784
+ todoListExtensionCommand
1785
+ .command('set')
1786
+ .description('Create an open extension on a list')
1787
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1788
+ .requiredOption('-n, --name <id>', 'extensionName')
1789
+ .requiredOption('--json-file <path>', 'JSON object: custom properties')
1790
+ .option('--json', 'Output as JSON')
1791
+ .option('--token <token>', 'Use a specific token')
1792
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1793
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1794
+ .action(
1795
+ async (
1796
+ opts: {
1797
+ list: string;
1798
+ name: string;
1799
+ jsonFile: string;
1800
+ json?: boolean;
1801
+ token?: string;
1802
+ identity?: string;
1803
+ user?: string;
1804
+ },
1805
+ cmd: any
1806
+ ) => {
1807
+ checkReadOnly(cmd);
1808
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1809
+ if (!auth.success) {
1810
+ console.error(`Auth error: ${auth.error}`);
1811
+ process.exit(1);
1812
+ }
1813
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1814
+ const raw = await readFile(opts.jsonFile, 'utf-8');
1815
+ const data = JSON.parse(raw) as Record<string, unknown>;
1816
+ const r = await setTodoListOpenExtension(auth.token!, listId, opts.name, data, opts.user);
1817
+ if (!r.ok || !r.data) {
1818
+ console.error(`Error: ${r.error?.message}`);
1819
+ process.exit(1);
1820
+ }
1821
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1822
+ else console.log(`\n\u2705 List extension set: ${opts.name}\n`);
1823
+ }
1824
+ );
1825
+
1826
+ todoListExtensionCommand
1827
+ .command('update')
1828
+ .description('PATCH a list open extension')
1829
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1830
+ .requiredOption('-n, --name <id>', 'extensionName')
1831
+ .requiredOption('--json-file <path>', 'JSON patch body')
1832
+ .option('--token <token>', 'Use a specific token')
1833
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1834
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1835
+ .action(
1836
+ async (
1837
+ opts: { list: string; name: string; jsonFile: string; token?: string; identity?: string; user?: string },
1838
+ cmd: any
1839
+ ) => {
1840
+ checkReadOnly(cmd);
1841
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1842
+ if (!auth.success) {
1843
+ console.error(`Auth error: ${auth.error}`);
1844
+ process.exit(1);
1845
+ }
1846
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1847
+ const raw = await readFile(opts.jsonFile, 'utf-8');
1848
+ const patch = JSON.parse(raw) as Record<string, unknown>;
1849
+ const r = await updateTodoListOpenExtension(auth.token!, listId, opts.name, patch, opts.user);
1850
+ if (!r.ok) {
1851
+ console.error(`Error: ${r.error?.message}`);
1852
+ process.exit(1);
1853
+ }
1854
+ console.log('\n\u2705 List extension updated.\n');
1855
+ }
1856
+ );
1857
+
1858
+ todoListExtensionCommand
1859
+ .command('delete')
1860
+ .description('Delete a list open extension')
1861
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1862
+ .requiredOption('-n, --name <id>', 'extensionName')
1863
+ .option('--confirm', 'Confirm without prompt')
1864
+ .option('--token <token>', 'Use a specific token')
1865
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1866
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1867
+ .action(
1868
+ async (
1869
+ opts: { list: string; name: string; confirm?: boolean; token?: string; identity?: string; user?: string },
1870
+ cmd: any
1871
+ ) => {
1872
+ checkReadOnly(cmd);
1873
+ if (!opts.confirm) {
1874
+ console.log(`Delete list extension "${opts.name}"? Run with --confirm.`);
1875
+ process.exit(1);
1876
+ }
1877
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1878
+ if (!auth.success) {
1879
+ console.error(`Auth error: ${auth.error}`);
1880
+ process.exit(1);
1881
+ }
1882
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1883
+ const r = await deleteTodoListOpenExtension(auth.token!, listId, opts.name, opts.user);
1884
+ if (!r.ok) {
1885
+ console.error(`Error: ${r.error?.message}`);
1886
+ process.exit(1);
1887
+ }
1888
+ console.log(`\n\u2705 Deleted list extension: ${opts.name}\n`);
1889
+ }
1890
+ );
1891
+
1892
+ todoCommand.addCommand(todoListExtensionCommand);
1893
+
1894
+ const todoExtensionCommand = new Command('extension').description('Open type extensions on a task (Graph)');
1895
+
1896
+ todoExtensionCommand
1897
+ .command('list')
1898
+ .description('List open extensions on a task')
1899
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1900
+ .requiredOption('-t, --task <id>', 'Task ID')
1901
+ .option('--json', 'Output as JSON')
1902
+ .option('--token <token>', 'Use a specific token')
1903
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1904
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1905
+ .action(
1906
+ async (opts: { list: string; task: string; json?: boolean; token?: string; identity?: string; user?: string }) => {
1907
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1908
+ if (!auth.success) {
1909
+ console.error(`Auth error: ${auth.error}`);
1910
+ process.exit(1);
1911
+ }
1912
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1913
+ const r = await listTaskOpenExtensions(auth.token!, listId, opts.task, opts.user);
1914
+ if (!r.ok || !r.data) {
1915
+ console.error(`Error: ${r.error?.message}`);
1916
+ process.exit(1);
1917
+ }
1918
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1919
+ else {
1920
+ for (const ext of r.data) {
1921
+ const name = (ext.extensionName as string) || JSON.stringify(ext);
1922
+ console.log(`- ${name}`);
1923
+ }
1924
+ }
1925
+ }
1926
+ );
1927
+
1928
+ todoExtensionCommand
1929
+ .command('get')
1930
+ .description('Get one open extension by name')
1931
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1932
+ .requiredOption('-t, --task <id>', 'Task ID')
1933
+ .requiredOption('-n, --name <id>', 'extensionName')
1934
+ .option('--json', 'Output as JSON')
1935
+ .option('--token <token>', 'Use a specific token')
1936
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1937
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1938
+ .action(
1939
+ async (opts: {
1940
+ list: string;
1941
+ task: string;
1942
+ name: string;
1943
+ json?: boolean;
1944
+ token?: string;
1945
+ identity?: string;
1946
+ user?: string;
1947
+ }) => {
1948
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1949
+ if (!auth.success) {
1950
+ console.error(`Auth error: ${auth.error}`);
1951
+ process.exit(1);
1952
+ }
1953
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1954
+ const r = await getTaskOpenExtension(auth.token!, listId, opts.task, opts.name, opts.user);
1955
+ if (!r.ok || !r.data) {
1956
+ console.error(`Error: ${r.error?.message}`);
1957
+ process.exit(1);
1958
+ }
1959
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1960
+ else console.log(JSON.stringify(r.data, null, 2));
1961
+ }
1962
+ );
1963
+
1964
+ todoExtensionCommand
1965
+ .command('set')
1966
+ .description('Create an open extension (POST); JSON file is merged with extensionName')
1967
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
1968
+ .requiredOption('-t, --task <id>', 'Task ID')
1969
+ .requiredOption('-n, --name <id>', 'extensionName')
1970
+ .requiredOption('--json-file <path>', 'JSON object: custom properties (extensionName added automatically)')
1971
+ .option('--json', 'Output as JSON')
1972
+ .option('--token <token>', 'Use a specific token')
1973
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1974
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
1975
+ .action(
1976
+ async (
1977
+ opts: {
1978
+ list: string;
1979
+ task: string;
1980
+ name: string;
1981
+ jsonFile: string;
1982
+ json?: boolean;
1983
+ token?: string;
1984
+ identity?: string;
1985
+ user?: string;
1986
+ },
1987
+ cmd: any
1988
+ ) => {
1989
+ checkReadOnly(cmd);
1990
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1991
+ if (!auth.success) {
1992
+ console.error(`Auth error: ${auth.error}`);
1993
+ process.exit(1);
1994
+ }
1995
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
1996
+ const raw = await readFile(opts.jsonFile, 'utf-8');
1997
+ const data = JSON.parse(raw) as Record<string, unknown>;
1998
+ const r = await setTaskOpenExtension(auth.token!, listId, opts.task, opts.name, data, opts.user);
1999
+ if (!r.ok || !r.data) {
2000
+ console.error(`Error: ${r.error?.message}`);
2001
+ process.exit(1);
2002
+ }
2003
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
2004
+ else console.log(`\n\u2705 Extension set: ${opts.name}\n`);
2005
+ }
2006
+ );
2007
+
2008
+ todoExtensionCommand
2009
+ .command('update')
2010
+ .description('PATCH an open extension (partial update)')
2011
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
2012
+ .requiredOption('-t, --task <id>', 'Task ID')
2013
+ .requiredOption('-n, --name <id>', 'extensionName')
2014
+ .requiredOption('--json-file <path>', 'JSON object: properties to patch')
2015
+ .option('--token <token>', 'Use a specific token')
2016
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
2017
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
2018
+ .action(
2019
+ async (
2020
+ opts: {
2021
+ list: string;
2022
+ task: string;
2023
+ name: string;
2024
+ jsonFile: string;
2025
+ token?: string;
2026
+ identity?: string;
2027
+ user?: string;
2028
+ },
2029
+ cmd: any
2030
+ ) => {
2031
+ checkReadOnly(cmd);
2032
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
2033
+ if (!auth.success) {
2034
+ console.error(`Auth error: ${auth.error}`);
2035
+ process.exit(1);
2036
+ }
2037
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
2038
+ const raw = await readFile(opts.jsonFile, 'utf-8');
2039
+ const patch = JSON.parse(raw) as Record<string, unknown>;
2040
+ const r = await updateTaskOpenExtension(auth.token!, listId, opts.task, opts.name, patch, opts.user);
2041
+ if (!r.ok) {
2042
+ console.error(`Error: ${r.error?.message}`);
2043
+ process.exit(1);
2044
+ }
2045
+ console.log('\n\u2705 Extension updated.\n');
2046
+ }
2047
+ );
2048
+
2049
+ todoExtensionCommand
2050
+ .command('delete')
2051
+ .description('Delete an open extension')
2052
+ .requiredOption('-l, --list <name|id>', 'List name or ID')
2053
+ .requiredOption('-t, --task <id>', 'Task ID')
2054
+ .requiredOption('-n, --name <id>', 'extensionName')
2055
+ .option('--confirm', 'Confirm without prompt')
2056
+ .option('--token <token>', 'Use a specific token')
2057
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
2058
+ .option('--user <email>', 'Target user or shared mailbox (Graph delegation)')
2059
+ .action(
2060
+ async (
2061
+ opts: {
2062
+ list: string;
2063
+ task: string;
2064
+ name: string;
2065
+ confirm?: boolean;
2066
+ token?: string;
2067
+ identity?: string;
2068
+ user?: string;
2069
+ },
2070
+ cmd: any
2071
+ ) => {
2072
+ checkReadOnly(cmd);
2073
+ if (!opts.confirm) {
2074
+ console.log(`Delete extension "${opts.name}"? Run with --confirm.`);
2075
+ process.exit(1);
2076
+ }
2077
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
2078
+ if (!auth.success) {
2079
+ console.error(`Auth error: ${auth.error}`);
2080
+ process.exit(1);
2081
+ }
2082
+ const { listId } = await resolveListId(auth.token!, opts.list, opts.user);
2083
+ const r = await deleteTaskOpenExtension(auth.token!, listId, opts.task, opts.name, opts.user);
2084
+ if (!r.ok) {
2085
+ console.error(`Error: ${r.error?.message}`);
2086
+ process.exit(1);
2087
+ }
2088
+ console.log(`\n\u2705 Deleted extension: ${opts.name}\n`);
2089
+ }
2090
+ );
2091
+
2092
+ todoCommand.addCommand(todoExtensionCommand);