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,204 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { Command } from 'commander';
3
+ import { AttachmentLinkSpecError, parseAttachLinkSpec } from '../lib/attach-link-spec.js';
4
+ import { AttachmentPathError, validateAttachmentPath } from '../lib/attachments.js';
5
+ import { resolveAuth } from '../lib/auth.js';
6
+ import { type EmailAttachment, type ReferenceAttachmentInput, sendEmail } from '../lib/ews-client.js';
7
+ import { markdownToHtml } from '../lib/markdown.js';
8
+ import { lookupMimeType } from '../lib/mime-type.js';
9
+ import { checkReadOnly } from '../lib/utils.js';
10
+
11
+ export const sendCommand = new Command('send')
12
+ .description('Send an email')
13
+ .requiredOption('--to <emails>', 'Recipient email(s), comma-separated')
14
+ .requiredOption('--subject <text>', 'Email subject')
15
+ .option('--body <text>', 'Email body', '')
16
+ .option('--category <name>', 'Category label (repeatable)', (v, acc) => [...acc, v], [] as string[])
17
+ .option('--cc <emails>', 'CC recipient(s), comma-separated')
18
+ .option('--bcc <emails>', 'BCC recipient(s), comma-separated')
19
+ .option('--attach <files>', 'Attach file(s), comma-separated paths')
20
+ .option(
21
+ '--attach-link <spec>',
22
+ 'Attach link: "Title|https://url" or bare https URL (repeatable)',
23
+ (v: string, prev: string[]) => [...prev, v],
24
+ [] as string[]
25
+ )
26
+ .option('--html', 'Send body as HTML')
27
+ .option('--markdown', 'Parse body as markdown (bold, links, lists)')
28
+ .option('--json', 'Output as JSON')
29
+ .option('--token <token>', 'Use a specific token')
30
+ .option('--mailbox <email>', 'Send from shared mailbox (Send As)')
31
+ .option('--identity <name>', 'Use a specific authentication identity (default: default)')
32
+ .action(
33
+ async (
34
+ options: {
35
+ to: string;
36
+ subject: string;
37
+ body?: string;
38
+ cc?: string;
39
+ bcc?: string;
40
+ attach?: string;
41
+ attachLink?: string[];
42
+ html?: boolean;
43
+ markdown?: boolean;
44
+ json?: boolean;
45
+ token?: string;
46
+ mailbox?: string;
47
+ identity?: string;
48
+ category?: string[];
49
+ },
50
+ cmd: any
51
+ ) => {
52
+ checkReadOnly(cmd);
53
+ const authResult = await resolveAuth({
54
+ token: options.token,
55
+ identity: options.identity
56
+ });
57
+
58
+ if (!authResult.success) {
59
+ if (options.json) {
60
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
61
+ } else {
62
+ console.error(`Error: ${authResult.error}`);
63
+ console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
64
+ }
65
+ process.exit(1);
66
+ }
67
+
68
+ const toList = options.to
69
+ .split(',')
70
+ .map((e) => e.trim())
71
+ .filter(Boolean);
72
+ const ccList = options.cc
73
+ ? options.cc
74
+ .split(',')
75
+ .map((e) => e.trim())
76
+ .filter(Boolean)
77
+ : undefined;
78
+ const bccList = options.bcc
79
+ ? options.bcc
80
+ .split(',')
81
+ .map((e) => e.trim())
82
+ .filter(Boolean)
83
+ : undefined;
84
+
85
+ if (toList.length === 0) {
86
+ console.error('At least one recipient is required.');
87
+ process.exit(1);
88
+ }
89
+
90
+ let body = options.body ?? '';
91
+ let bodyType: 'Text' | 'HTML' = 'Text';
92
+
93
+ if (options.markdown) {
94
+ body = markdownToHtml(body);
95
+ bodyType = 'HTML';
96
+ } else if (options.html) {
97
+ bodyType = 'HTML';
98
+ }
99
+
100
+ // Process attachments
101
+ let attachments: EmailAttachment[] | undefined;
102
+ const workingDirectory = process.cwd();
103
+ if (options.attach) {
104
+ const filePaths = options.attach
105
+ .split(',')
106
+ .map((f) => f.trim())
107
+ .filter(Boolean);
108
+ attachments = [];
109
+
110
+ for (const filePath of filePaths) {
111
+ try {
112
+ const validated = await validateAttachmentPath(filePath, workingDirectory);
113
+ const content = await readFile(validated.absolutePath);
114
+ const contentType = lookupMimeType(validated.fileName);
115
+
116
+ attachments.push({
117
+ name: validated.fileName,
118
+ contentType,
119
+ contentBytes: content.toString('base64')
120
+ });
121
+
122
+ if (!options.json) {
123
+ console.log(` Attaching: ${validated.fileName} (${Math.round(validated.size / 1024)} KB)`);
124
+ }
125
+ } catch (err) {
126
+ console.error(`Failed to read attachment: ${filePath}`);
127
+ if (err instanceof AttachmentPathError) {
128
+ console.error(err.message);
129
+ } else {
130
+ console.error(err instanceof Error ? err.message : 'Unknown error');
131
+ }
132
+ process.exit(1);
133
+ }
134
+ }
135
+ }
136
+
137
+ let referenceAttachments: ReferenceAttachmentInput[] | undefined;
138
+ const linkSpecs = options.attachLink ?? [];
139
+ if (linkSpecs.length > 0) {
140
+ referenceAttachments = [];
141
+ for (const spec of linkSpecs) {
142
+ try {
143
+ const { name, url } = parseAttachLinkSpec(spec);
144
+ referenceAttachments.push({ name, url, contentType: 'text/html' });
145
+ if (!options.json) {
146
+ console.log(` Attaching link: ${name}`);
147
+ }
148
+ } catch (err) {
149
+ const msg =
150
+ err instanceof AttachmentLinkSpecError ? err.message : err instanceof Error ? err.message : String(err);
151
+ console.error(`Invalid --attach-link: ${msg}`);
152
+ process.exit(1);
153
+ }
154
+ }
155
+ }
156
+
157
+ const result = await sendEmail(authResult.token!, {
158
+ to: toList,
159
+ cc: ccList,
160
+ bcc: bccList,
161
+ subject: options.subject,
162
+ body,
163
+ bodyType,
164
+ attachments,
165
+ referenceAttachments,
166
+ mailbox: options.mailbox,
167
+ categories: options.category && options.category.length > 0 ? options.category : undefined
168
+ });
169
+
170
+ if (!result.ok) {
171
+ if (options.json) {
172
+ console.log(JSON.stringify({ error: result.error?.message || 'Failed to send email' }, null, 2));
173
+ } else {
174
+ console.error(`Error: ${result.error?.message || 'Failed to send email'}`);
175
+ }
176
+ process.exit(1);
177
+ }
178
+
179
+ if (options.json) {
180
+ console.log(
181
+ JSON.stringify(
182
+ {
183
+ success: true,
184
+ to: toList,
185
+ subject: options.subject,
186
+ attachments: attachments?.map((a) => a.name),
187
+ attachLinks: referenceAttachments?.map((a) => a.name)
188
+ },
189
+ null,
190
+ 2
191
+ )
192
+ );
193
+ } else {
194
+ console.log(`\n\u2713 Email sent to ${toList.join(', ')}`);
195
+ console.log(` Subject: ${options.subject}`);
196
+ const nFile = attachments?.length ?? 0;
197
+ const nLink = referenceAttachments?.length ?? 0;
198
+ if (nFile + nLink > 0) {
199
+ console.log(` Attachments: ${nFile} file(s), ${nLink} link(s)`);
200
+ }
201
+ console.log();
202
+ }
203
+ }
204
+ );
@@ -0,0 +1,14 @@
1
+ import { Command } from 'commander';
2
+ import { startWebhookServer } from '../lib/webhook-server.js';
3
+
4
+ export const serveCommand = new Command('serve')
5
+ .description('Start the webhook receiver server')
6
+ .option('-p, --port <port>', 'Port to listen on', '3000')
7
+ .action((options) => {
8
+ const port = parseInt(options.port, 10);
9
+ if (Number.isNaN(port) || port <= 0 || port > 65535) {
10
+ console.error(`Invalid port "${options.port}". Please provide an integer between 1 and 65535.`);
11
+ process.exit(1);
12
+ }
13
+ startWebhookServer(port);
14
+ });
@@ -0,0 +1,179 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+ import { createListItem, getListItems, getLists, updateListItem } from '../lib/sharepoint-client.js';
4
+ import { checkReadOnly } from '../lib/utils.js';
5
+
6
+ export const sharepointCommand = new Command('sharepoint').description('Manage Microsoft SharePoint Lists').alias('sp');
7
+
8
+ sharepointCommand
9
+ .command('lists')
10
+ .description('List all SharePoint lists in a site')
11
+ .requiredOption('--site-id <id>', 'SharePoint Site ID')
12
+ .option('--json', 'Output as JSON')
13
+ .option('--token <token>', 'Use a specific token')
14
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
15
+ .action(async (opts: { siteId: string; json?: boolean; token?: string; identity?: string }) => {
16
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
17
+ if (!auth.success || !auth.token) {
18
+ console.error(`Auth error: ${auth.error || 'Unknown error'}`);
19
+ process.exit(1);
20
+ }
21
+ const res = await getLists(auth.token, opts.siteId);
22
+ if (!res.ok) {
23
+ console.error(`Error listing lists: ${res.error?.message || 'Unknown error'}`);
24
+ process.exit(1);
25
+ }
26
+ if (opts.json) {
27
+ console.log(JSON.stringify(res.data, null, 2));
28
+ return;
29
+ }
30
+ if (!res.data || res.data.length === 0) {
31
+ console.log('No lists found in this site.');
32
+ return;
33
+ }
34
+ for (const list of res.data) {
35
+ console.log(`${list.name} (${list.id})`);
36
+ if (list.description) console.log(` ${list.description}`);
37
+ }
38
+ });
39
+
40
+ sharepointCommand
41
+ .command('items')
42
+ .description('Get items from a SharePoint list')
43
+ .requiredOption('--site-id <id>', 'SharePoint Site ID')
44
+ .requiredOption('--list-id <id>', 'SharePoint List ID')
45
+ .option('--json', 'Output as JSON')
46
+ .option('--token <token>', 'Use a specific token')
47
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
48
+ .action(async (opts: { siteId: string; listId: string; json?: boolean; token?: string; identity?: string }) => {
49
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
50
+ if (!auth.success || !auth.token) {
51
+ console.error(`Auth error: ${auth.error || 'Unknown error'}`);
52
+ process.exit(1);
53
+ }
54
+ const res = await getListItems(auth.token, opts.siteId, opts.listId);
55
+ if (!res.ok) {
56
+ console.error(`Error getting list items: ${res.error?.message || 'Unknown error'}`);
57
+ process.exit(1);
58
+ }
59
+ if (opts.json) {
60
+ console.log(JSON.stringify(res.data, null, 2));
61
+ return;
62
+ }
63
+ if (!res.data || res.data.length === 0) {
64
+ console.log('No items found in this list.');
65
+ return;
66
+ }
67
+ for (const item of res.data) {
68
+ console.log(`Item ID: ${item.id}`);
69
+ if (item.fields) {
70
+ for (const [key, val] of Object.entries(item.fields)) {
71
+ if (!key.startsWith('@odata')) {
72
+ console.log(` ${key}: ${val}`);
73
+ }
74
+ }
75
+ }
76
+ console.log('---');
77
+ }
78
+ });
79
+
80
+ sharepointCommand
81
+ .command('create-item')
82
+ .description('Create an item in a SharePoint list')
83
+ .requiredOption('--site-id <id>', 'SharePoint Site ID')
84
+ .requiredOption('--list-id <id>', 'SharePoint List ID')
85
+ .requiredOption('--fields <json>', 'JSON string of fields to set (e.g. \'{"Title": "My Item"}\')')
86
+ .option('--json', 'Output as JSON')
87
+ .option('--token <token>', 'Use a specific token')
88
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
89
+ .action(
90
+ async (
91
+ opts: { siteId: string; listId: string; fields: string; json?: boolean; token?: string; identity?: string },
92
+ cmd: any
93
+ ) => {
94
+ checkReadOnly(cmd);
95
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
96
+ if (!auth.success || !auth.token) {
97
+ console.error(`Auth error: ${auth.error || 'Unknown error'}`);
98
+ process.exit(1);
99
+ }
100
+ let parsedFields: Record<string, any>;
101
+ try {
102
+ parsedFields = JSON.parse(opts.fields);
103
+ } catch (err: any) {
104
+ console.error(`Error parsing fields JSON: ${err.message}`);
105
+ process.exit(1);
106
+ }
107
+ if (typeof parsedFields !== 'object' || parsedFields === null || Array.isArray(parsedFields)) {
108
+ console.error(
109
+ 'Error: --fields JSON must be an object (e.g. "{"Title": "New Title"}"), not an array or primitive.'
110
+ );
111
+ process.exit(1);
112
+ }
113
+ const res = await createListItem(auth.token, opts.siteId, opts.listId, parsedFields);
114
+ if (!res.ok) {
115
+ console.error(`Error creating list item: ${res.error?.message || 'Unknown error'}`);
116
+ process.exit(1);
117
+ }
118
+ if (opts.json) {
119
+ console.log(JSON.stringify(res.data, null, 2));
120
+ return;
121
+ }
122
+ console.log(`Successfully created item ${res.data?.id}`);
123
+ }
124
+ );
125
+
126
+ sharepointCommand
127
+ .command('update-item')
128
+ .description('Update an item in a SharePoint list')
129
+ .requiredOption('--site-id <id>', 'SharePoint Site ID')
130
+ .requiredOption('--list-id <id>', 'SharePoint List ID')
131
+ .requiredOption('--item-id <id>', 'SharePoint List Item ID')
132
+ .requiredOption('--fields <json>', 'JSON string of fields to set (e.g. \'{"Title": "New Title"}\')')
133
+ .option('--json', 'Output as JSON')
134
+ .option('--token <token>', 'Use a specific token')
135
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
136
+ .action(
137
+ async (
138
+ opts: {
139
+ siteId: string;
140
+ listId: string;
141
+ itemId: string;
142
+ fields: string;
143
+ json?: boolean;
144
+ token?: string;
145
+ identity?: string;
146
+ },
147
+ cmd: any
148
+ ) => {
149
+ checkReadOnly(cmd);
150
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
151
+ if (!auth.success || !auth.token) {
152
+ console.error(`Auth error: ${auth.error || 'Unknown error'}`);
153
+ process.exit(1);
154
+ }
155
+ let parsedFields: Record<string, any>;
156
+ try {
157
+ parsedFields = JSON.parse(opts.fields);
158
+ } catch (err: any) {
159
+ console.error(`Error parsing fields JSON: ${err.message}`);
160
+ process.exit(1);
161
+ }
162
+ if (typeof parsedFields !== 'object' || parsedFields === null || Array.isArray(parsedFields)) {
163
+ console.error(
164
+ 'Error: --fields JSON must be an object (e.g. "{"Title": "New Title"}"), not an array or primitive.'
165
+ );
166
+ process.exit(1);
167
+ }
168
+ const res = await updateListItem(auth.token, opts.siteId, opts.listId, opts.itemId, parsedFields);
169
+ if (!res.ok) {
170
+ console.error(`Error updating list item: ${res.error?.message || 'Unknown error'}`);
171
+ process.exit(1);
172
+ }
173
+ if (opts.json) {
174
+ console.log(JSON.stringify(res.data, null, 2));
175
+ return;
176
+ }
177
+ console.log(`Successfully updated item ${opts.itemId}`);
178
+ }
179
+ );
@@ -0,0 +1,163 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+ import {
4
+ getSitePage,
5
+ listSitePages,
6
+ publishSitePage,
7
+ type SitePage,
8
+ updateSitePage
9
+ } from '../lib/site-pages-client.js';
10
+ import { checkReadOnly } from '../lib/utils.js';
11
+
12
+ export const sitePagesCommand = new Command('pages').description('Manage SharePoint Site Pages');
13
+
14
+ sitePagesCommand
15
+ .command('list <siteId>')
16
+ .description('List site pages for a given site ID')
17
+ .option('--json', 'Output as JSON')
18
+ .option('--token <token>', 'Use a specific Graph token')
19
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
20
+ .action(async (siteId: string, options: { json?: boolean; token?: string; identity?: string }) => {
21
+ const auth = await resolveGraphAuth({ token: options.token, identity: options.identity });
22
+ if (!auth.success) {
23
+ console.error(`Error: ${auth.error}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const result = await listSitePages(auth.token!, siteId);
28
+ if (!result.ok || !result.data) {
29
+ console.error(`Error: ${result.error?.message || 'Request failed'}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ if (options.json) {
34
+ console.log(JSON.stringify(result.data, null, 2));
35
+ return;
36
+ }
37
+
38
+ if (!result.data || result.data.length === 0) {
39
+ console.log('No pages found.');
40
+ return;
41
+ }
42
+
43
+ for (const page of result.data) {
44
+ const state = page.publishingState
45
+ ? `${page.publishingState.level} (v${page.publishingState.versionId})`
46
+ : 'Unknown';
47
+ console.log(`- ${page.name || page.title || page.id} (${page.id}) - State: ${state}`);
48
+ }
49
+ });
50
+
51
+ sitePagesCommand
52
+ .command('get <siteId> <pageId>')
53
+ .description('Get a site page by ID')
54
+ .option('--json', 'Output as JSON')
55
+ .option('--token <token>', 'Use a specific Graph token')
56
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
57
+ .action(async (siteId: string, pageId: string, options: { json?: boolean; token?: string; identity?: string }) => {
58
+ const auth = await resolveGraphAuth({ token: options.token, identity: options.identity });
59
+ if (!auth.success) {
60
+ console.error(`Error: ${auth.error}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ const result = await getSitePage(auth.token!, siteId, pageId);
65
+ if (!result.ok || !result.data) {
66
+ console.error(`Error: ${result.error?.message || 'Request failed'}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ if (options.json) {
71
+ console.log(JSON.stringify(result.data, null, 2));
72
+ return;
73
+ }
74
+
75
+ console.log(`ID: ${result.data.id}`);
76
+ console.log(`Name: ${result.data.name || '-'}`);
77
+ console.log(`Title: ${result.data.title || '-'}`);
78
+ console.log(`Web URL: ${result.data.webUrl || '-'}`);
79
+ if (result.data.publishingState) {
80
+ console.log(`State: ${result.data.publishingState.level} (v${result.data.publishingState.versionId})`);
81
+ }
82
+ });
83
+
84
+ sitePagesCommand
85
+ .command('update <siteId> <pageId>')
86
+ .description('Update a site page')
87
+ .option('--title <title>', 'New title')
88
+ .option('--name <name>', 'New name')
89
+ .option('--json', 'Output as JSON')
90
+ .option('--token <token>', 'Use a specific Graph token')
91
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
92
+ .action(
93
+ async (
94
+ siteId: string,
95
+ pageId: string,
96
+ options: { title?: string; name?: string; json?: boolean; token?: string; identity?: string },
97
+ cmd: any
98
+ ) => {
99
+ checkReadOnly(cmd);
100
+ const auth = await resolveGraphAuth({ token: options.token, identity: options.identity });
101
+ if (!auth.success) {
102
+ console.error(`Error: ${auth.error}`);
103
+ process.exit(1);
104
+ }
105
+
106
+ const payload: Partial<SitePage> = {};
107
+ if (options.title) payload.title = options.title;
108
+ if (options.name) payload.name = options.name;
109
+
110
+ if (Object.keys(payload).length === 0) {
111
+ console.error('Error: Please provide at least one field to update (--title or --name)');
112
+ process.exit(1);
113
+ }
114
+
115
+ const result = await updateSitePage(auth.token!, siteId, pageId, payload);
116
+ if (!result.ok || !result.data) {
117
+ console.error(`Error: ${result.error?.message || 'Request failed'}`);
118
+ process.exit(1);
119
+ }
120
+
121
+ if (options.json) {
122
+ console.log(JSON.stringify(result.data, null, 2));
123
+ return;
124
+ }
125
+
126
+ console.log(`✓ Updated page ${pageId}`);
127
+ }
128
+ );
129
+
130
+ sitePagesCommand
131
+ .command('publish <siteId> <pageId>')
132
+ .description('Publish a site page')
133
+ .option('--json', 'Output as JSON')
134
+ .option('--token <token>', 'Use a specific Graph token')
135
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
136
+ .action(
137
+ async (
138
+ siteId: string,
139
+ pageId: string,
140
+ options: { json?: boolean; token?: string; identity?: string },
141
+ cmd: any
142
+ ) => {
143
+ checkReadOnly(cmd);
144
+ const auth = await resolveGraphAuth({ token: options.token, identity: options.identity });
145
+ if (!auth.success) {
146
+ console.error(`Error: ${auth.error}`);
147
+ process.exit(1);
148
+ }
149
+
150
+ const result = await publishSitePage(auth.token!, siteId, pageId);
151
+ if (!result.ok) {
152
+ console.error(`Error: ${result.error?.message || 'Request failed'}`);
153
+ process.exit(1);
154
+ }
155
+
156
+ if (options.json) {
157
+ console.log(JSON.stringify({ ok: true }, null, 2));
158
+ return;
159
+ }
160
+
161
+ console.log(`✓ Published page ${pageId}`);
162
+ }
163
+ );
@@ -0,0 +1,103 @@
1
+ import { Command } from 'commander';
2
+ import { createSubscription, deleteSubscription } from '../lib/graph-subscriptions.js';
3
+ import { checkReadOnly } from '../lib/utils.js';
4
+
5
+ export const subscribeCommand = new Command('subscribe')
6
+ .description('Subscribe to Microsoft Graph push notifications')
7
+ .argument('[resource]', 'Resource to subscribe to (e.g. mail, event, contact, todoTask)')
8
+ .option('--url <url>', 'Webhook notification URL')
9
+ .option('--expiry <datetime>', 'Expiration datetime (ISO 8601, defaults to 3 days from now)')
10
+ .option('--change-type <type>', 'Change type (comma-separated)', 'created,updated')
11
+ .option('--token <token>', 'Use a specific token')
12
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
13
+ .option('--user <email>', 'Subscribe under this user or shared mailbox (users/{id}/...)')
14
+ .action(async (resource, options, cmd) => {
15
+ if (!resource) {
16
+ return cmd.help();
17
+ }
18
+ if (!options.url) {
19
+ console.error('Error: --url is required.');
20
+ process.exit(1);
21
+ }
22
+
23
+ checkReadOnly(cmd);
24
+
25
+ // Map friendly resource names to graph endpoints
26
+ const mapResource = (res: string, user?: string) => {
27
+ const prefix = user?.trim() ? `users/${encodeURIComponent(user.trim())}` : 'me';
28
+ switch (res.toLowerCase()) {
29
+ case 'mail':
30
+ return `${prefix}/messages`;
31
+ case 'event':
32
+ return `${prefix}/events`;
33
+ case 'contact':
34
+ return `${prefix}/contacts`;
35
+ case 'todotask':
36
+ // Note: Todo subscriptions require a specific list ID.
37
+ // Use the format: me/todo/lists/{listId}/tasks
38
+ // For the default Tasks list, use: me/todo/lists/Tasks/tasks
39
+ return `${prefix}/todo/lists/Tasks/tasks`;
40
+ default:
41
+ return res;
42
+ }
43
+ };
44
+
45
+ // Generate clientState for subscription validation (if GRAPH_CLIENT_STATE env is set)
46
+ const clientState = process.env.GRAPH_CLIENT_STATE;
47
+
48
+ const graphResource = mapResource(resource, options.user);
49
+
50
+ // Default expiration to 3 days (Graph allows up to 3 days for most resources)
51
+ let expiry = options.expiry;
52
+ if (!expiry) {
53
+ const date = new Date();
54
+ date.setDate(date.getDate() + 3);
55
+ // Ensure we don't exceed max limits by shaving off a minute
56
+ date.setMinutes(date.getMinutes() - 1);
57
+ expiry = date.toISOString();
58
+ }
59
+
60
+ try {
61
+ console.log(`Creating subscription for ${graphResource}...`);
62
+ const res = await createSubscription(
63
+ graphResource,
64
+ options.changeType,
65
+ options.url,
66
+ expiry,
67
+ clientState,
68
+ options.token,
69
+ options.identity
70
+ );
71
+ if (!res.ok) {
72
+ console.error(`Failed to create subscription: ${res.error?.message}`);
73
+ process.exit(1);
74
+ }
75
+ const sub = res.data;
76
+ console.log('Subscription created successfully!');
77
+ console.log(JSON.stringify(sub, null, 2));
78
+ } catch (err) {
79
+ console.error(err instanceof Error ? err.message : err);
80
+ process.exit(1);
81
+ }
82
+ });
83
+
84
+ subscribeCommand
85
+ .command('cancel <id>')
86
+ .description('Cancel an existing subscription')
87
+ .option('--token <token>', 'Use a specific token')
88
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
89
+ .action(async (id, options, cmd) => {
90
+ try {
91
+ checkReadOnly(cmd);
92
+ console.log(`Deleting subscription ${id}...`);
93
+ const res = await deleteSubscription(id, options.token, options.identity);
94
+ if (!res.ok) {
95
+ console.error(`Failed to delete subscription: ${res.error?.message}`);
96
+ process.exit(1);
97
+ }
98
+ console.log('Subscription deleted successfully.');
99
+ } catch (err) {
100
+ console.error(err instanceof Error ? err.message : err);
101
+ process.exit(1);
102
+ }
103
+ });
@@ -0,0 +1,29 @@
1
+ import { Command } from 'commander';
2
+ import { listSubscriptions } from '../lib/graph-subscriptions.js';
3
+
4
+ export const subscriptionsCommand = new Command('subscriptions').description('Manage Microsoft Graph subscriptions');
5
+
6
+ subscriptionsCommand
7
+ .command('list')
8
+ .description('List all active subscriptions')
9
+ .option('--token <token>', 'Use a specific token')
10
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
11
+ .action(async (options: { token?: string; identity?: string }) => {
12
+ try {
13
+ const res = await listSubscriptions(options.token, options.identity);
14
+ if (!res.ok || !res.data) {
15
+ console.error(`Failed to list subscriptions: ${res.error?.message}`);
16
+ process.exit(1);
17
+ }
18
+ const subs = res.data;
19
+ if (subs.length === 0) {
20
+ console.log('No active subscriptions found.');
21
+ return;
22
+ }
23
+ console.log(`Found ${subs.length} active subscription(s):`);
24
+ console.log(JSON.stringify(subs, null, 2));
25
+ } catch (err) {
26
+ console.error(err instanceof Error ? err.message : err);
27
+ process.exit(1);
28
+ }
29
+ });