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,217 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+ import {
4
+ type GraphCalendarEvent,
5
+ getCalendar,
6
+ getEvent,
7
+ listCalendars,
8
+ listCalendarView
9
+ } from '../lib/graph-calendar-client.js';
10
+ import { acceptEventInvitation, declineEventInvitation, tentativelyAcceptEventInvitation } from '../lib/graph-event.js';
11
+ import { checkReadOnly } from '../lib/utils.js';
12
+
13
+ export const graphCalendarCommand = new Command('graph-calendar').description(
14
+ 'Microsoft Graph calendar REST: calendars, calendarView, events, invitation responses (distinct from EWS `calendar` / `respond`)'
15
+ );
16
+
17
+ function formatEventLine(e: GraphCalendarEvent): string {
18
+ const subj = e.subject?.trim() || '(no subject)';
19
+ const start = e.start?.dateTime;
20
+ const end = e.end?.dateTime;
21
+ const tz = e.start?.timeZone || '';
22
+ const when =
23
+ start && end ? `${start} → ${end}${tz ? ` (${tz})` : ''}` : start ? `${start}${tz ? ` (${tz})` : ''}` : '?';
24
+ const allDay = e.isAllDay ? ' [all-day]' : '';
25
+ return `${when}${allDay}\t${subj}\t${e.id}`;
26
+ }
27
+
28
+ graphCalendarCommand
29
+ .command('list-calendars')
30
+ .description('List calendars (Graph GET /calendars)')
31
+ .option('--json', 'Output as JSON')
32
+ .option('--token <token>', 'Graph access token')
33
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
34
+ .option('--user <email>', 'Target mailbox (delegation)')
35
+ .action(async (opts: { json?: boolean; token?: string; identity?: string; user?: string }) => {
36
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
37
+ if (!auth.success || !auth.token) {
38
+ console.error(`Auth error: ${auth.error}`);
39
+ process.exit(1);
40
+ }
41
+ const r = await listCalendars(auth.token, opts.user);
42
+ if (!r.ok || !r.data) {
43
+ console.error(`Error: ${r.error?.message}`);
44
+ process.exit(1);
45
+ }
46
+ if (opts.json) {
47
+ console.log(JSON.stringify(r.data, null, 2));
48
+ return;
49
+ }
50
+ for (const c of r.data) {
51
+ const label = c.name || '(unnamed)';
52
+ console.log(`${label}\t${c.id}`);
53
+ }
54
+ });
55
+
56
+ graphCalendarCommand
57
+ .command('get-calendar')
58
+ .description('Get one calendar by id')
59
+ .argument('<calendarId>', 'Calendar id')
60
+ .option('--json', 'Output as JSON')
61
+ .option('--token <token>', 'Graph access token')
62
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
63
+ .option('--user <email>', 'Target mailbox (delegation)')
64
+ .action(async (calendarId: string, opts: { json?: boolean; token?: string; identity?: string; user?: string }) => {
65
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
66
+ if (!auth.success || !auth.token) {
67
+ console.error(`Auth error: ${auth.error}`);
68
+ process.exit(1);
69
+ }
70
+ const r = await getCalendar(auth.token, calendarId, opts.user);
71
+ if (!r.ok || !r.data) {
72
+ console.error(`Error: ${r.error?.message}`);
73
+ process.exit(1);
74
+ }
75
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
76
+ else console.log(JSON.stringify(r.data, null, 2));
77
+ });
78
+
79
+ graphCalendarCommand
80
+ .command('list-view')
81
+ .description('List events in a time window (Graph GET .../calendarView)')
82
+ .requiredOption('--start <iso>', 'Start (ISO 8601, e.g. 2026-04-01T00:00:00Z)')
83
+ .requiredOption('--end <iso>', 'End (ISO 8601, exclusive upper bound in many cases — see Graph docs)')
84
+ .option('-c, --calendar <calendarId>', 'Calendar id (omit for default calendar)')
85
+ .option('--json', 'Output as JSON')
86
+ .option('--token <token>', 'Graph access token')
87
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
88
+ .option('--user <email>', 'Target mailbox (delegation)')
89
+ .action(
90
+ async (opts: {
91
+ start: string;
92
+ end: string;
93
+ calendar?: string;
94
+ json?: boolean;
95
+ token?: string;
96
+ identity?: string;
97
+ user?: string;
98
+ }) => {
99
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
100
+ if (!auth.success || !auth.token) {
101
+ console.error(`Auth error: ${auth.error}`);
102
+ process.exit(1);
103
+ }
104
+ const r = await listCalendarView(auth.token, opts.start, opts.end, {
105
+ calendarId: opts.calendar,
106
+ user: opts.user
107
+ });
108
+ if (!r.ok || !r.data) {
109
+ console.error(`Error: ${r.error?.message}`);
110
+ process.exit(1);
111
+ }
112
+ if (opts.json) {
113
+ console.log(JSON.stringify(r.data, null, 2));
114
+ return;
115
+ }
116
+ for (const e of r.data) {
117
+ console.log(formatEventLine(e));
118
+ }
119
+ }
120
+ );
121
+
122
+ graphCalendarCommand
123
+ .command('get-event')
124
+ .description('Get a single event by id (Graph GET /events/{id})')
125
+ .argument('<eventId>', 'Event id')
126
+ .option('--select <fields>', 'OData $select (comma-separated)')
127
+ .option('--json', 'Output as JSON')
128
+ .option('--token <token>', 'Graph access token')
129
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
130
+ .option('--user <email>', 'Mailbox that owns the event (delegation)')
131
+ .action(
132
+ async (
133
+ eventId: string,
134
+ opts: {
135
+ select?: string;
136
+ json?: boolean;
137
+ token?: string;
138
+ identity?: string;
139
+ user?: string;
140
+ }
141
+ ) => {
142
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
143
+ if (!auth.success || !auth.token) {
144
+ console.error(`Auth error: ${auth.error}`);
145
+ process.exit(1);
146
+ }
147
+ const r = await getEvent(auth.token, eventId, opts.user, opts.select);
148
+ if (!r.ok || !r.data) {
149
+ console.error(`Error: ${r.error?.message}`);
150
+ process.exit(1);
151
+ }
152
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
153
+ else console.log(JSON.stringify(r.data, null, 2));
154
+ }
155
+ );
156
+
157
+ function addRespondCommand(
158
+ name: string,
159
+ description: string,
160
+ fn: (o: {
161
+ token: string;
162
+ eventId: string;
163
+ comment?: string;
164
+ sendResponse: boolean;
165
+ user?: string;
166
+ }) => ReturnType<typeof acceptEventInvitation>
167
+ ) {
168
+ graphCalendarCommand
169
+ .command(name)
170
+ .description(description)
171
+ .argument('<eventId>', 'Event id')
172
+ .option('--comment <text>', 'Optional comment to organizer')
173
+ .option('--no-notify', "Don't send response to organizer")
174
+ .option('--token <token>', 'Graph access token')
175
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
176
+ .option('--user <email>', 'Mailbox that owns the invitation (delegation)')
177
+ .action(
178
+ async (
179
+ eventId: string,
180
+ opts: {
181
+ comment?: string;
182
+ notify: boolean;
183
+ token?: string;
184
+ identity?: string;
185
+ user?: string;
186
+ },
187
+ cmd: any
188
+ ) => {
189
+ checkReadOnly(cmd);
190
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
191
+ if (!auth.success || !auth.token) {
192
+ console.error(`Auth error: ${auth.error}`);
193
+ process.exit(1);
194
+ }
195
+ const r = await fn({
196
+ token: auth.token,
197
+ eventId,
198
+ comment: opts.comment,
199
+ sendResponse: opts.notify !== false,
200
+ user: opts.user
201
+ });
202
+ if (!r.ok) {
203
+ console.error(`Error: ${r.error?.message}`);
204
+ process.exit(1);
205
+ }
206
+ console.log('Done.');
207
+ }
208
+ );
209
+ }
210
+
211
+ addRespondCommand('accept', 'Accept a meeting request (Graph POST .../accept)', acceptEventInvitation);
212
+ addRespondCommand('decline', 'Decline a meeting request (Graph POST .../decline)', declineEventInvitation);
213
+ addRespondCommand(
214
+ 'tentative',
215
+ 'Tentatively accept without proposing a new time (Graph POST .../tentativelyAccept)',
216
+ tentativelyAcceptEventInvitation
217
+ );
@@ -0,0 +1,195 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { createInterface } from 'node:readline/promises';
6
+ import { Command } from 'commander';
7
+ import { atomicWriteUtf8File } from '../lib/atomic-write.js';
8
+ import { getMicrosoftTenantPathSegment } from '../lib/jwt-utils.js';
9
+
10
+ async function performDeviceCodeFlow(clientId: string, tenant: string, scope: string, label: string): Promise<string> {
11
+ console.log(`\nInitiating Device Code flow for ${label}...`);
12
+
13
+ const deviceCodeRes = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/devicecode`, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
16
+ body: new URLSearchParams({
17
+ client_id: clientId,
18
+ scope: scope
19
+ }).toString()
20
+ });
21
+
22
+ const deviceCodeJson = await deviceCodeRes.json();
23
+
24
+ if (!deviceCodeRes.ok) {
25
+ console.error(`Failed to initiate ${label} device code flow:`, deviceCodeJson);
26
+ process.exit(1);
27
+ }
28
+
29
+ console.log('\n=========================================================');
30
+ console.log(deviceCodeJson.message);
31
+ console.log('=========================================================\n');
32
+
33
+ const deviceCode = deviceCodeJson.device_code;
34
+ const interval = (deviceCodeJson.interval || 5) * 1000;
35
+ const expiresAt = Date.now() + (deviceCodeJson.expires_in || 900) * 1000;
36
+
37
+ let authenticated = false;
38
+ let refreshToken = '';
39
+ let pollInterval = interval;
40
+
41
+ console.log(`Waiting for ${label} authentication...`);
42
+
43
+ while (!authenticated) {
44
+ if (Date.now() > expiresAt) {
45
+ console.error(`\n${label} device code expired. Please run the command again.`);
46
+ process.exit(1);
47
+ }
48
+
49
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
50
+
51
+ const tokenRes = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
54
+ body: new URLSearchParams({
55
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
56
+ client_id: clientId,
57
+ device_code: deviceCode
58
+ }).toString()
59
+ });
60
+
61
+ const tokenJson = await tokenRes.json();
62
+
63
+ if (tokenRes.ok) {
64
+ authenticated = true;
65
+ refreshToken = tokenJson.refresh_token;
66
+ // Extract username from access token
67
+
68
+ try {
69
+ const parts = tokenJson.access_token.split('.');
70
+
71
+ if (parts.length === 3) {
72
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
73
+
74
+ const rawUsername = payload.upn || payload.email;
75
+ const username = rawUsername ? rawUsername.replace(/[\r\n]/g, '') : undefined;
76
+
77
+ if (username) {
78
+ let envContent = '';
79
+
80
+ const configDir = join(homedir(), '.config', 'm365-agent-cli');
81
+ mkdirSync(configDir, { recursive: true, mode: 0o700 });
82
+ const envPath = join(configDir, '.env');
83
+
84
+ try {
85
+ envContent = await readFile(envPath, 'utf8');
86
+ } catch (err: any) {
87
+ if (err.code !== 'ENOENT') throw err;
88
+ }
89
+
90
+ if (/^EWS_USERNAME=.*$/m.test(envContent)) {
91
+ envContent = envContent.replace(/^EWS_USERNAME=.*$/m, () => `EWS_USERNAME=${username}`);
92
+ } else {
93
+ envContent += `\nEWS_USERNAME=${username}\n`;
94
+ }
95
+
96
+ await atomicWriteUtf8File(envPath, `${envContent.trim()}\n`, 0o600);
97
+
98
+ console.log(`Saved EWS_USERNAME (${username}) to ${envPath}`);
99
+ }
100
+ }
101
+ } catch (_e) {
102
+ /* ignore parse errors */
103
+ }
104
+ if (!refreshToken) {
105
+ console.error(`\nFailed to obtain ${label} refresh token. Ensure the offline_access scope is granted.`);
106
+ process.exit(1);
107
+ }
108
+ } else if (tokenJson.error === 'authorization_pending') {
109
+ // Continue polling
110
+ } else if (tokenJson.error === 'slow_down') {
111
+ pollInterval += 5000;
112
+ } else {
113
+ console.error(`\n${label} authentication failed:`, tokenJson.error_description || tokenJson.error);
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ console.log(`\n${label} authentication successful!`);
119
+
120
+ return refreshToken;
121
+ }
122
+
123
+ export const loginCommand = new Command('login')
124
+ .description('Interactive login to obtain refresh tokens via OAuth2 Device Code flow')
125
+ .action(async () => {
126
+ let clientId = process.env.EWS_CLIENT_ID;
127
+
128
+ // Read existing .env if present
129
+ const configDir = join(homedir(), '.config', 'm365-agent-cli');
130
+ mkdirSync(configDir, { recursive: true, mode: 0o700 });
131
+ const envPath = join(configDir, '.env');
132
+ let envContent = '';
133
+ if (existsSync(envPath)) {
134
+ envContent = await readFile(envPath, 'utf8');
135
+ if (!clientId) {
136
+ const match = envContent.match(/^EWS_CLIENT_ID=(.*)$/m);
137
+ if (match) {
138
+ clientId = match[1].trim();
139
+ }
140
+ }
141
+ }
142
+
143
+ if (!clientId) {
144
+ const rl = createInterface({
145
+ input: process.stdin,
146
+ output: process.stdout
147
+ });
148
+ clientId = await rl.question('Enter your EWS_CLIENT_ID: ');
149
+ rl.close();
150
+ clientId = clientId.trim();
151
+
152
+ if (!clientId) {
153
+ console.error('EWS_CLIENT_ID is required.');
154
+ process.exit(1);
155
+ }
156
+
157
+ // Save it to .env
158
+ envContent += `\nEWS_CLIENT_ID=${clientId}\n`;
159
+ await atomicWriteUtf8File(envPath, `${envContent.trim()}\n`, 0o600);
160
+ }
161
+
162
+ const tenant = getMicrosoftTenantPathSegment();
163
+
164
+ // Use a single Graph Device Code flow to obtain a multi-resource refresh token
165
+ const graphScope =
166
+ 'offline_access User.Read Calendars.ReadWrite Mail.ReadWrite Files.ReadWrite.All Sites.ReadWrite.All Tasks.ReadWrite Group.ReadWrite.All';
167
+ const rawToken = await performDeviceCodeFlow(clientId, tenant, graphScope, 'Microsoft 365');
168
+ const refreshToken = rawToken.replace(/[\r\n]/g, '');
169
+
170
+ // Save tokens immediately
171
+ try {
172
+ envContent = await readFile(envPath, 'utf8');
173
+ } catch (err: any) {
174
+ if (err.code !== 'ENOENT') throw err;
175
+ }
176
+
177
+ // Update or append EWS_REFRESH_TOKEN
178
+ if (/^EWS_REFRESH_TOKEN=.*$/m.test(envContent)) {
179
+ envContent = envContent.replace(/^EWS_REFRESH_TOKEN=.*$/m, () => `EWS_REFRESH_TOKEN=${refreshToken}`);
180
+ } else {
181
+ envContent += `\nEWS_REFRESH_TOKEN=${refreshToken}\n`;
182
+ }
183
+
184
+ // Update or append GRAPH_REFRESH_TOKEN
185
+ if (/^GRAPH_REFRESH_TOKEN=.*$/m.test(envContent)) {
186
+ envContent = envContent.replace(/^GRAPH_REFRESH_TOKEN=.*$/m, () => `GRAPH_REFRESH_TOKEN=${refreshToken}`);
187
+ } else {
188
+ envContent += `\nGRAPH_REFRESH_TOKEN=${refreshToken}\n`;
189
+ }
190
+
191
+ envContent = envContent.replace(/\n{3,}/g, '\n\n');
192
+ await atomicWriteUtf8File(envPath, `${envContent.trim()}\n`, 0o600);
193
+
194
+ console.log(`Saved GRAPH_REFRESH_TOKEN and EWS_REFRESH_TOKEN to ${envPath}`);
195
+ });