@codemieai/code 0.0.47 → 0.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/plugins/claude/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/agents/plugins/claude/plugin/skills/msgraph/README.md +35 -36
- package/dist/agents/plugins/claude/plugin/skills/msgraph/SKILL.md +39 -42
- package/dist/agents/plugins/claude/plugin/skills/msgraph/scripts/msgraph.js +791 -0
- package/dist/agents/plugins/claude/plugin/skills/report-issue/SKILL.md +288 -0
- package/package.json +1 -1
- package/dist/agents/plugins/claude/plugin/skills/msgraph/scripts/msgraph.py +0 -785
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* msgraph.js — Microsoft Graph API CLI for Claude Code skill
|
|
4
|
+
*
|
|
5
|
+
* Authentication: OAuth2 device code flow with persistent token cache.
|
|
6
|
+
* First time: node msgraph.js login
|
|
7
|
+
* Subsequent: token refreshed silently from cache.
|
|
8
|
+
*
|
|
9
|
+
* Dependencies: node >= 18 (built-in modules only — zero npm installs needed)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const https = require('node:https');
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
|
|
19
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
20
|
+
const CLIENT_ID = '14d82eec-204b-4c2f-b7e8-296a70dab67e';
|
|
21
|
+
const TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
|
|
22
|
+
const DEVICE_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode';
|
|
23
|
+
const SCOPES = [
|
|
24
|
+
'User.Read', 'Mail.Read', 'Mail.Send',
|
|
25
|
+
'Calendars.Read', 'Calendars.ReadWrite',
|
|
26
|
+
'Files.Read', 'Files.ReadWrite',
|
|
27
|
+
'Sites.Read.All', 'Chat.Read', 'Chat.ReadWrite',
|
|
28
|
+
'People.Read', 'Contacts.Read', 'offline_access',
|
|
29
|
+
].join(' ');
|
|
30
|
+
const CACHE_FILE = path.join(os.homedir(), '.ms_graph_token_cache.json');
|
|
31
|
+
const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
|
|
32
|
+
|
|
33
|
+
// ── HTTP Helpers ──────────────────────────────────────────────────────────────
|
|
34
|
+
function httpsRequest(urlStr, options = {}, body = null) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const u = new URL(urlStr);
|
|
37
|
+
const req = https.request({
|
|
38
|
+
hostname: u.hostname,
|
|
39
|
+
path: u.pathname + u.search,
|
|
40
|
+
method: options.method || 'GET',
|
|
41
|
+
headers: options.headers || {},
|
|
42
|
+
}, res => {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
res.on('data', c => chunks.push(c));
|
|
45
|
+
res.on('end', () => {
|
|
46
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
47
|
+
if (res.statusCode >= 400) {
|
|
48
|
+
const err = new Error(`HTTP ${res.statusCode}`);
|
|
49
|
+
err.statusCode = res.statusCode;
|
|
50
|
+
err.responseBody = text;
|
|
51
|
+
err.responseUrl = urlStr;
|
|
52
|
+
return reject(err);
|
|
53
|
+
}
|
|
54
|
+
resolve({ status: res.statusCode, body: text, headers: res.headers });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
req.on('error', reject);
|
|
58
|
+
if (body) req.write(body);
|
|
59
|
+
req.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function oauthPost(urlStr, params) {
|
|
64
|
+
const body = new URLSearchParams(params).toString();
|
|
65
|
+
const res = await httpsRequest(urlStr, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
69
|
+
'Content-Length': Buffer.byteLength(body),
|
|
70
|
+
},
|
|
71
|
+
}, body);
|
|
72
|
+
return JSON.parse(res.body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function graphGet(endpoint, token, params = {}) {
|
|
76
|
+
const qs = new URLSearchParams(params).toString();
|
|
77
|
+
const url = `${GRAPH_BASE}${endpoint}${qs ? '?' + qs : ''}`;
|
|
78
|
+
const res = await httpsRequest(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
79
|
+
return JSON.parse(res.body);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function graphPost(endpoint, token, body) {
|
|
83
|
+
const bodyStr = JSON.stringify(body);
|
|
84
|
+
const res = await httpsRequest(`${GRAPH_BASE}${endpoint}`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${token}`,
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
90
|
+
},
|
|
91
|
+
}, bodyStr);
|
|
92
|
+
return res.body ? JSON.parse(res.body) : {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Download file content, following 302 redirects (Graph uses CDN redirects). */
|
|
96
|
+
function graphDownload(endpoint, token) {
|
|
97
|
+
function fetch(url, auth) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const u = new URL(url);
|
|
100
|
+
const headers = auth ? { Authorization: `Bearer ${auth}` } : {};
|
|
101
|
+
https.get({ hostname: u.hostname, path: u.pathname + u.search, headers }, res => {
|
|
102
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
103
|
+
res.resume();
|
|
104
|
+
fetch(res.headers.location, null).then(resolve, reject);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const chunks = [];
|
|
108
|
+
res.on('data', c => chunks.push(c));
|
|
109
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
110
|
+
res.on('error', reject);
|
|
111
|
+
}).on('error', reject);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return fetch(`${GRAPH_BASE}${endpoint}`, token);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function graphUpload(endpoint, token, content, contentType = 'application/octet-stream') {
|
|
118
|
+
const res = await httpsRequest(`${GRAPH_BASE}${endpoint}`, {
|
|
119
|
+
method: 'PUT',
|
|
120
|
+
headers: {
|
|
121
|
+
Authorization: `Bearer ${token}`,
|
|
122
|
+
'Content-Type': contentType,
|
|
123
|
+
'Content-Length': content.length,
|
|
124
|
+
},
|
|
125
|
+
}, content);
|
|
126
|
+
return JSON.parse(res.body);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Token Cache ───────────────────────────────────────────────────────────────
|
|
130
|
+
function loadCache() {
|
|
131
|
+
if (!fs.existsSync(CACHE_FILE)) return null;
|
|
132
|
+
try { return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); } catch { return null; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function saveCache(data) {
|
|
136
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Authentication ────────────────────────────────────────────────────────────
|
|
140
|
+
async function tryRefresh(refreshTkn, username) {
|
|
141
|
+
try {
|
|
142
|
+
const res = await oauthPost(TOKEN_URL, {
|
|
143
|
+
client_id: CLIENT_ID,
|
|
144
|
+
grant_type: 'refresh_token',
|
|
145
|
+
refresh_token: refreshTkn,
|
|
146
|
+
scope: SCOPES,
|
|
147
|
+
});
|
|
148
|
+
if (res.access_token) {
|
|
149
|
+
saveCache({
|
|
150
|
+
access_token: res.access_token,
|
|
151
|
+
refresh_token: res.refresh_token || refreshTkn,
|
|
152
|
+
expires_at: Math.floor(Date.now() / 1000) + (res.expires_in || 3600),
|
|
153
|
+
username: username || '',
|
|
154
|
+
});
|
|
155
|
+
return res.access_token;
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Returns a valid token, silently refreshing if needed. Exits if not logged in. */
|
|
162
|
+
async function getValidToken() {
|
|
163
|
+
const cache = loadCache();
|
|
164
|
+
if (!cache?.access_token) {
|
|
165
|
+
console.log('NOT_LOGGED_IN');
|
|
166
|
+
process.exit(2);
|
|
167
|
+
}
|
|
168
|
+
const now = Math.floor(Date.now() / 1000);
|
|
169
|
+
if (!cache.expires_at || now < cache.expires_at - 60) return cache.access_token;
|
|
170
|
+
if (cache.refresh_token) {
|
|
171
|
+
const t = await tryRefresh(cache.refresh_token, cache.username);
|
|
172
|
+
if (t) return t;
|
|
173
|
+
}
|
|
174
|
+
console.log('TOKEN_EXPIRED');
|
|
175
|
+
process.exit(2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Like getValidToken but returns null instead of exiting (used by status cmd). */
|
|
179
|
+
async function tryGetToken() {
|
|
180
|
+
const cache = loadCache();
|
|
181
|
+
if (!cache?.access_token) return null;
|
|
182
|
+
const now = Math.floor(Date.now() / 1000);
|
|
183
|
+
if (!cache.expires_at || now < cache.expires_at - 60) return cache.access_token;
|
|
184
|
+
if (cache.refresh_token) return tryRefresh(cache.refresh_token, cache.username);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function getAccessToken(forceLogin = false) {
|
|
189
|
+
if (!forceLogin) {
|
|
190
|
+
const cache = loadCache();
|
|
191
|
+
if (cache?.access_token) {
|
|
192
|
+
const now = Math.floor(Date.now() / 1000);
|
|
193
|
+
if (cache.expires_at && now < cache.expires_at - 60) return cache.access_token;
|
|
194
|
+
if (cache.refresh_token) {
|
|
195
|
+
const t = await tryRefresh(cache.refresh_token, cache.username);
|
|
196
|
+
if (t) return t;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Device Code Flow
|
|
202
|
+
const device = await oauthPost(DEVICE_URL, { client_id: CLIENT_ID, scope: SCOPES });
|
|
203
|
+
if (!device.device_code) throw new Error(`Device flow failed: ${JSON.stringify(device)}`);
|
|
204
|
+
|
|
205
|
+
console.log('\n' + '='.repeat(60));
|
|
206
|
+
console.log(device.message);
|
|
207
|
+
console.log('='.repeat(60) + '\n');
|
|
208
|
+
|
|
209
|
+
const interval = (device.interval || 5) * 1000;
|
|
210
|
+
const deadline = Date.now() + (device.expires_in || 900) * 1000;
|
|
211
|
+
|
|
212
|
+
while (Date.now() < deadline) {
|
|
213
|
+
await new Promise(r => setTimeout(r, interval));
|
|
214
|
+
try {
|
|
215
|
+
const res = await oauthPost(TOKEN_URL, {
|
|
216
|
+
client_id: CLIENT_ID,
|
|
217
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
218
|
+
device_code: device.device_code,
|
|
219
|
+
});
|
|
220
|
+
if (res.access_token) {
|
|
221
|
+
const me = await graphGet('/me', res.access_token, { $select: 'userPrincipalName' });
|
|
222
|
+
saveCache({
|
|
223
|
+
access_token: res.access_token,
|
|
224
|
+
refresh_token: res.refresh_token || '',
|
|
225
|
+
expires_at: Math.floor(Date.now() / 1000) + (res.expires_in || 3600),
|
|
226
|
+
username: me.userPrincipalName || '',
|
|
227
|
+
});
|
|
228
|
+
return res.access_token;
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
let body = {};
|
|
232
|
+
try { body = JSON.parse(err.responseBody || '{}'); } catch {}
|
|
233
|
+
if (body.error === 'authorization_pending') continue;
|
|
234
|
+
if (body.error === 'slow_down') { await new Promise(r => setTimeout(r, 5000)); continue; }
|
|
235
|
+
if (body.error === 'expired_token') throw new Error('Device code expired. Run login again.');
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw new Error('Authentication timed out.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Formatters ────────────────────────────────────────────────────────────────
|
|
243
|
+
function fmtDt(iso) {
|
|
244
|
+
if (!iso) return 'N/A';
|
|
245
|
+
try { return new Date(iso).toISOString().slice(0, 16).replace('T', ' '); }
|
|
246
|
+
catch { return (iso || '').slice(0, 16).replace('T', ' '); }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fmtSize(n) {
|
|
250
|
+
if (!n) return '';
|
|
251
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
252
|
+
let i = 0;
|
|
253
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
254
|
+
return `${n.toFixed(1)} ${units[i]}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function stripHtml(s) {
|
|
258
|
+
return (s || '')
|
|
259
|
+
.replace(/<[^>]*>/g, '')
|
|
260
|
+
.replace(/ /g, ' ')
|
|
261
|
+
.replace(/&/g, '&')
|
|
262
|
+
.replace(/</g, '<')
|
|
263
|
+
.replace(/>/g, '>')
|
|
264
|
+
.replace(/\r?\n\s*\r?\n/g, '\n')
|
|
265
|
+
.trim();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function pad(str, len) {
|
|
269
|
+
str = String(str || '');
|
|
270
|
+
return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
274
|
+
async function cmdLogin() {
|
|
275
|
+
console.log('Starting Microsoft authentication...');
|
|
276
|
+
const token = await getAccessToken(true);
|
|
277
|
+
const me = await graphGet('/me', token);
|
|
278
|
+
console.log(`\nLogged in as: ${me.displayName} <${me.userPrincipalName}>`);
|
|
279
|
+
console.log(`User ID: ${me.id}`);
|
|
280
|
+
console.log(`Token cached at: ${CACHE_FILE}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function cmdLogout() {
|
|
284
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
285
|
+
fs.unlinkSync(CACHE_FILE);
|
|
286
|
+
console.log(`Logged out. Cache removed: ${CACHE_FILE}`);
|
|
287
|
+
} else {
|
|
288
|
+
console.log('No cached credentials found.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function cmdStatus() {
|
|
293
|
+
const cache = loadCache();
|
|
294
|
+
if (!cache?.access_token) {
|
|
295
|
+
console.log('NOT_LOGGED_IN');
|
|
296
|
+
console.log(`\nTo login, run:\n node ${path.basename(process.argv[1])} login`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const token = await tryGetToken();
|
|
300
|
+
if (token) {
|
|
301
|
+
const updated = loadCache();
|
|
302
|
+
console.log(`Logged in as: ${updated?.username || cache.username}`);
|
|
303
|
+
console.log(`Cache file: ${CACHE_FILE}`);
|
|
304
|
+
} else {
|
|
305
|
+
console.log('TOKEN_EXPIRED');
|
|
306
|
+
console.log(`Account: ${cache.username}`);
|
|
307
|
+
console.log(`\nSession expired. Re-authenticate with:\n node ${path.basename(process.argv[1])} login`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function cmdMe(args) {
|
|
312
|
+
const token = await getValidToken();
|
|
313
|
+
const me = await graphGet('/me', token);
|
|
314
|
+
if (args.json) {
|
|
315
|
+
const fields = ['displayName','userPrincipalName','id','mail','jobTitle',
|
|
316
|
+
'department','officeLocation','businessPhones','mobilePhone'];
|
|
317
|
+
const out = {};
|
|
318
|
+
for (const k of fields) if (me[k] != null) out[k] = me[k];
|
|
319
|
+
console.log(JSON.stringify(out, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
console.log(`Name : ${me.displayName || 'N/A'}`);
|
|
323
|
+
console.log(`Email : ${me.userPrincipalName || 'N/A'}`);
|
|
324
|
+
console.log(`Job Title : ${me.jobTitle || 'N/A'}`);
|
|
325
|
+
console.log(`Department : ${me.department || 'N/A'}`);
|
|
326
|
+
console.log(`Office : ${me.officeLocation || 'N/A'}`);
|
|
327
|
+
console.log(`Phone : ${(me.businessPhones || [])[0] || me.mobilePhone || 'N/A'}`);
|
|
328
|
+
console.log(`User ID : ${me.id || 'N/A'}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function cmdEmails(args) {
|
|
332
|
+
const token = await getValidToken();
|
|
333
|
+
|
|
334
|
+
if (args.send) {
|
|
335
|
+
await graphPost('/me/sendMail', token, {
|
|
336
|
+
message: {
|
|
337
|
+
subject: args.subject || '(no subject)',
|
|
338
|
+
body: { contentType: 'Text', content: args.body || '' },
|
|
339
|
+
toRecipients: [{ emailAddress: { address: args.send } }],
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
console.log(`Email sent to ${args.send}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (args.read) {
|
|
347
|
+
const msg = await graphGet(`/me/messages/${args.read}`, token);
|
|
348
|
+
const from = msg.from?.emailAddress || {};
|
|
349
|
+
console.log(`Subject : ${msg.subject}`);
|
|
350
|
+
console.log(`From : ${from.name || ''} <${from.address || ''}>`);
|
|
351
|
+
console.log(`Date : ${fmtDt(msg.receivedDateTime)}`);
|
|
352
|
+
console.log(`Read : ${msg.isRead ? 'Yes' : 'No'}`);
|
|
353
|
+
console.log(`\n${'─'.repeat(60)}`);
|
|
354
|
+
const body = msg.body || {};
|
|
355
|
+
console.log(body.contentType === 'text' ? body.content : stripHtml(body.content || '').slice(0, 2000));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const limit = parseInt(args.limit) || 10;
|
|
360
|
+
const params = {
|
|
361
|
+
$top: limit,
|
|
362
|
+
$select: 'id,subject,from,receivedDateTime,isRead,hasAttachments,importance',
|
|
363
|
+
$orderby: 'receivedDateTime desc',
|
|
364
|
+
};
|
|
365
|
+
if (args.search) { params.$search = `"${args.search}"`; delete params.$orderby; }
|
|
366
|
+
if (args.unread) params.$filter = 'isRead eq false';
|
|
367
|
+
|
|
368
|
+
const endpoint = args.folder ? `/me/mailFolders/${args.folder}/messages` : '/me/messages';
|
|
369
|
+
const data = await graphGet(endpoint, token, params);
|
|
370
|
+
const emails = data.value || [];
|
|
371
|
+
|
|
372
|
+
if (args.json) { console.log(JSON.stringify(emails, null, 2)); return; }
|
|
373
|
+
if (!emails.length) { console.log('No emails found.'); return; }
|
|
374
|
+
|
|
375
|
+
console.log(`\n${'ID'.padEnd(36)} ${'Date'.padEnd(16)} Rd Subject`);
|
|
376
|
+
console.log('─'.repeat(80));
|
|
377
|
+
for (const e of emails) {
|
|
378
|
+
const mark = e.isRead ? '✓' : '●';
|
|
379
|
+
const att = e.hasAttachments ? '📎' : ' ';
|
|
380
|
+
const subject = (e.subject || '(no subject)').slice(0, 45);
|
|
381
|
+
const sender = (e.from?.emailAddress?.name || '').slice(0, 20);
|
|
382
|
+
console.log(`${e.id.slice(0,36)} ${fmtDt(e.receivedDateTime).padEnd(16)} ${mark} ${att} ${subject} (${sender})`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function cmdCalendar(args) {
|
|
387
|
+
const token = await getValidToken();
|
|
388
|
+
|
|
389
|
+
if (args.create) {
|
|
390
|
+
if (!args.start || !args.end) {
|
|
391
|
+
console.error('Error: --create requires --start and --end (format: YYYY-MM-DDTHH:MM)');
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
const tz = args.timezone || 'UTC';
|
|
395
|
+
const payload = {
|
|
396
|
+
subject: args.create,
|
|
397
|
+
start: { dateTime: args.start, timeZone: tz },
|
|
398
|
+
end: { dateTime: args.end, timeZone: tz },
|
|
399
|
+
};
|
|
400
|
+
if (args.location) payload.location = { displayName: args.location };
|
|
401
|
+
if (args.body) payload.body = { contentType: 'Text', content: args.body };
|
|
402
|
+
const event = await graphPost('/me/events', token, payload);
|
|
403
|
+
console.log(`Event created: ${event.subject}`);
|
|
404
|
+
console.log(`ID: ${event.id}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (args.availability) {
|
|
409
|
+
const now = new Date();
|
|
410
|
+
const start = args.start || now.toISOString().slice(0, 19);
|
|
411
|
+
const end = args.end || new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59).toISOString().slice(0, 19);
|
|
412
|
+
const view = await graphGet('/me/calendarView', token, {
|
|
413
|
+
startDateTime: start,
|
|
414
|
+
endDateTime: end,
|
|
415
|
+
$select: 'subject,start,end,showAs',
|
|
416
|
+
$orderby: 'start/dateTime',
|
|
417
|
+
});
|
|
418
|
+
const events = view.value || [];
|
|
419
|
+
if (!events.length) {
|
|
420
|
+
console.log(`You're free between ${start.slice(0,16)} and ${end.slice(0,16)}.`);
|
|
421
|
+
} else {
|
|
422
|
+
console.log(`\nBusy slots (${events.length} events):`);
|
|
423
|
+
for (const e of events)
|
|
424
|
+
console.log(` ${fmtDt(e.start.dateTime)} — ${fmtDt(e.end.dateTime)}: ${e.subject || '(no title)'}`);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const limit = parseInt(args.limit) || 10;
|
|
430
|
+
const now = new Date();
|
|
431
|
+
const yearEnd = new Date(now.getFullYear() + 1, 0, 1);
|
|
432
|
+
const data = await graphGet('/me/calendarView', token, {
|
|
433
|
+
startDateTime: now.toISOString().slice(0, 19) + 'Z',
|
|
434
|
+
endDateTime: yearEnd.toISOString().slice(0, 19) + 'Z',
|
|
435
|
+
$top: limit,
|
|
436
|
+
$select: 'id,subject,start,end,location,organizer,isOnlineMeeting',
|
|
437
|
+
$orderby: 'start/dateTime',
|
|
438
|
+
});
|
|
439
|
+
const events = data.value || [];
|
|
440
|
+
|
|
441
|
+
if (args.json) { console.log(JSON.stringify(events, null, 2)); return; }
|
|
442
|
+
if (!events.length) { console.log('No upcoming events.'); return; }
|
|
443
|
+
|
|
444
|
+
console.log(`\n${'Date & Time'.padEnd(20)} Title`);
|
|
445
|
+
console.log('─'.repeat(80));
|
|
446
|
+
for (const e of events) {
|
|
447
|
+
const online = e.isOnlineMeeting ? '🎥' : ' ';
|
|
448
|
+
const title = (e.subject || '(no title)').slice(0, 45);
|
|
449
|
+
const organizer = (e.organizer?.emailAddress?.name || '').slice(0, 20);
|
|
450
|
+
const loc = (e.location?.displayName || '').slice(0, 20);
|
|
451
|
+
console.log(`${fmtDt(e.start?.dateTime).padEnd(20)} ${online} ${title} (${organizer})${loc ? ` @ ${loc}` : ''}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function cmdSharepoint(args) {
|
|
456
|
+
const token = await getValidToken();
|
|
457
|
+
const limit = parseInt(args.limit) || 20;
|
|
458
|
+
|
|
459
|
+
if (args.sites) {
|
|
460
|
+
let data = await graphGet('/me/followedSites', token, { $select: 'id,displayName,webUrl' });
|
|
461
|
+
let sites = data.value || [];
|
|
462
|
+
if (!sites.length) {
|
|
463
|
+
data = await graphGet('/sites', token, { search: '*', $top: limit });
|
|
464
|
+
sites = data.value || [];
|
|
465
|
+
}
|
|
466
|
+
if (args.json) { console.log(JSON.stringify(sites, null, 2)); return; }
|
|
467
|
+
console.log(`\n${'ID'.padEnd(50)} Name`);
|
|
468
|
+
console.log('─'.repeat(80));
|
|
469
|
+
for (const s of sites.slice(0, limit)) {
|
|
470
|
+
console.log(`${(s.id || '').padEnd(50)} ${s.displayName || 'N/A'}`);
|
|
471
|
+
console.log(` URL: ${s.webUrl || 'N/A'}`);
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (args.site) {
|
|
477
|
+
const p = args.path || 'root';
|
|
478
|
+
const ep = p === 'root'
|
|
479
|
+
? `/sites/${args.site}/drive/root/children`
|
|
480
|
+
: `/sites/${args.site}/drive/root:/${p}:/children`;
|
|
481
|
+
const data = await graphGet(ep, token, { $top: limit, $select: 'id,name,size,lastModifiedDateTime,file,folder' });
|
|
482
|
+
const items = data.value || [];
|
|
483
|
+
if (args.json) { console.log(JSON.stringify(items, null, 2)); return; }
|
|
484
|
+
console.log(`\nFiles in site ${args.site} / ${p}:`);
|
|
485
|
+
console.log('─'.repeat(60));
|
|
486
|
+
for (const item of items) {
|
|
487
|
+
const kind = item.folder ? '📁' : '📄';
|
|
488
|
+
const size = item.file ? fmtSize(item.size) : '';
|
|
489
|
+
const modified = fmtDt(item.lastModifiedDateTime);
|
|
490
|
+
console.log(` ${kind} ${pad(item.name, 40)} ${pad(size, 10)} ${modified}`);
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (args.download) {
|
|
496
|
+
const outPath = args.output || `downloaded_${args.download.slice(0, 8)}`;
|
|
497
|
+
const content = await graphDownload(`/me/drive/items/${args.download}/content`, token);
|
|
498
|
+
fs.writeFileSync(outPath, content);
|
|
499
|
+
console.log(`Downloaded ${content.length} bytes to ${outPath}`);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
console.log('SharePoint: --sites | --site SITE_ID [--path PATH] | --download ITEM_ID [--output FILE]');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function cmdTeams(args) {
|
|
507
|
+
const token = await getValidToken();
|
|
508
|
+
const limit = parseInt(args.limit) || 20;
|
|
509
|
+
|
|
510
|
+
if (args.chats) {
|
|
511
|
+
const data = await graphGet('/me/chats', token, { $top: limit, $select: 'id,topic,chatType,lastUpdatedDateTime' });
|
|
512
|
+
const chats = data.value || [];
|
|
513
|
+
if (args.json) { console.log(JSON.stringify(chats, null, 2)); return; }
|
|
514
|
+
console.log(`\n${'Chat ID'.padEnd(50)} ${'Type'.padEnd(10)} Topic`);
|
|
515
|
+
console.log('─'.repeat(80));
|
|
516
|
+
for (const c of chats)
|
|
517
|
+
console.log(`${(c.id || '').padEnd(50)} ${(c.chatType || '').padEnd(10)} ${c.topic || '(direct message)'}`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (args.messages) {
|
|
522
|
+
const data = await graphGet(`/me/chats/${args.messages}/messages`, token, {
|
|
523
|
+
$top: limit, $select: 'id,from,body,createdDateTime',
|
|
524
|
+
});
|
|
525
|
+
const msgs = data.value || [];
|
|
526
|
+
if (args.json) { console.log(JSON.stringify(msgs, null, 2)); return; }
|
|
527
|
+
console.log(`\nMessages in chat ${args.messages.slice(0, 20)}...:`);
|
|
528
|
+
console.log('─'.repeat(60));
|
|
529
|
+
for (const m of [...msgs].reverse()) {
|
|
530
|
+
const sender = m.from?.user?.displayName || 'System';
|
|
531
|
+
const body = stripHtml(m.body?.content || '').slice(0, 200);
|
|
532
|
+
console.log(`[${fmtDt(m.createdDateTime)}] ${sender}: ${body}`);
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (args.send && args.chatId) {
|
|
538
|
+
const res = await graphPost(`/me/chats/${args.chatId}/messages`, token, { body: { content: args.send } });
|
|
539
|
+
console.log(`Message sent. ID: ${res.id}`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (args.teamsList) {
|
|
544
|
+
const data = await graphGet('/me/joinedTeams', token, { $select: 'id,displayName,description' });
|
|
545
|
+
const teams = data.value || [];
|
|
546
|
+
if (args.json) { console.log(JSON.stringify(teams, null, 2)); return; }
|
|
547
|
+
for (const t of teams) console.log(`${t.id.slice(0, 36)} ${t.displayName}`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
console.log('Teams: --chats | --messages CHAT_ID | --send MSG --chat-id ID | --teams-list');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function cmdOnedrive(args) {
|
|
555
|
+
const token = await getValidToken();
|
|
556
|
+
const limit = parseInt(args.limit) || 20;
|
|
557
|
+
|
|
558
|
+
if (args.upload) {
|
|
559
|
+
if (!fs.existsSync(args.upload)) {
|
|
560
|
+
console.error(`File not found: ${args.upload}`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
const content = fs.readFileSync(args.upload);
|
|
564
|
+
const dest = args.dest || path.basename(args.upload);
|
|
565
|
+
const result = await graphUpload(`/me/drive/root:/${dest}:/content`, token, content);
|
|
566
|
+
console.log(`Uploaded: ${result.name} (${fmtSize(result.size)})`);
|
|
567
|
+
console.log(`ID: ${result.id}`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (args.download) {
|
|
572
|
+
const outPath = args.output || `download_${args.download.slice(0, 8)}`;
|
|
573
|
+
const content = await graphDownload(`/me/drive/items/${args.download}/content`, token);
|
|
574
|
+
fs.writeFileSync(outPath, content);
|
|
575
|
+
console.log(`Downloaded ${fmtSize(content.length)} to ${outPath}`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (args.info) {
|
|
580
|
+
const item = await graphGet(`/me/drive/items/${args.info}`, token);
|
|
581
|
+
if (args.json) { console.log(JSON.stringify(item, null, 2)); return; }
|
|
582
|
+
console.log(`Name : ${item.name}`);
|
|
583
|
+
console.log(`Size : ${fmtSize(item.size)}`);
|
|
584
|
+
console.log(`Modified: ${fmtDt(item.lastModifiedDateTime)}`);
|
|
585
|
+
console.log(`URL : ${item.webUrl || 'N/A'}`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const p = args.path || '';
|
|
590
|
+
const ep = p ? `/me/drive/root:/${p}:/children` : '/me/drive/root/children';
|
|
591
|
+
const data = await graphGet(ep, token, { $top: limit, $select: 'id,name,size,lastModifiedDateTime,file,folder', $orderby: 'name' });
|
|
592
|
+
const items = data.value || [];
|
|
593
|
+
|
|
594
|
+
if (args.json) { console.log(JSON.stringify(items, null, 2)); return; }
|
|
595
|
+
if (!items.length) { console.log(`No files found in /${p || ''}`); return; }
|
|
596
|
+
|
|
597
|
+
console.log(`\nOneDrive: /${p || ''}`);
|
|
598
|
+
console.log('─'.repeat(60));
|
|
599
|
+
for (const item of items) {
|
|
600
|
+
const kind = item.folder ? '📁' : '📄';
|
|
601
|
+
const size = item.file ? fmtSize(item.size) : '';
|
|
602
|
+
const modified = fmtDt(item.lastModifiedDateTime);
|
|
603
|
+
const count = item.folder ? ` (${item.folder.childCount} items)` : '';
|
|
604
|
+
console.log(` ${kind} ${item.id.slice(0,16)} ${pad(item.name, 40)} ${pad(size, 10)} ${modified}${count}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function cmdPeople(args) {
|
|
609
|
+
const token = await getValidToken();
|
|
610
|
+
const limit = parseInt(args.limit) || 20;
|
|
611
|
+
|
|
612
|
+
if (args.contacts) {
|
|
613
|
+
const params = { $top: limit, $select: 'displayName,emailAddresses,mobilePhone,jobTitle,companyName' };
|
|
614
|
+
if (args.search) params.$search = `"${args.search}"`;
|
|
615
|
+
const data = await graphGet('/me/contacts', token, params);
|
|
616
|
+
const contacts = data.value || [];
|
|
617
|
+
if (args.json) { console.log(JSON.stringify(contacts, null, 2)); return; }
|
|
618
|
+
console.log(`\n${'Name'.padEnd(30)} ${'Email'.padEnd(35)} Title`);
|
|
619
|
+
console.log('─'.repeat(80));
|
|
620
|
+
for (const c of contacts) {
|
|
621
|
+
const email = (c.emailAddresses || [])[0]?.address || 'N/A';
|
|
622
|
+
console.log(`${pad(c.displayName || '', 30)} ${pad(email, 35)} ${c.jobTitle || ''}`);
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const params = { $top: limit };
|
|
628
|
+
if (args.search) params.$search = `"${args.search}"`;
|
|
629
|
+
const data = await graphGet('/me/people', token, params);
|
|
630
|
+
const people = data.value || [];
|
|
631
|
+
|
|
632
|
+
if (args.json) { console.log(JSON.stringify(people, null, 2)); return; }
|
|
633
|
+
if (!people.length) { console.log('No people found.'); return; }
|
|
634
|
+
|
|
635
|
+
console.log(`\n${'Name'.padEnd(30)} ${'Email'.padEnd(35)} Title`);
|
|
636
|
+
console.log('─'.repeat(80));
|
|
637
|
+
for (const p of people) {
|
|
638
|
+
const email = (p.scoredEmailAddresses || [])[0]?.address || 'N/A';
|
|
639
|
+
console.log(`${pad(p.displayName || '', 30)} ${pad(email, 35)} ${p.jobTitle || ''}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function cmdOrg(args) {
|
|
644
|
+
const token = await getValidToken();
|
|
645
|
+
|
|
646
|
+
if (args.manager) {
|
|
647
|
+
try {
|
|
648
|
+
const mgr = await graphGet('/me/manager', token);
|
|
649
|
+
console.log(`Manager: ${mgr.displayName} <${mgr.userPrincipalName}>`);
|
|
650
|
+
console.log(`Title : ${mgr.jobTitle || 'N/A'}`);
|
|
651
|
+
} catch (err) {
|
|
652
|
+
if (err.statusCode === 404) console.log('No manager found (you may be at the top of the org).');
|
|
653
|
+
else throw err;
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (args.reports) {
|
|
659
|
+
const data = await graphGet('/me/directReports', token, { $select: 'displayName,userPrincipalName,jobTitle' });
|
|
660
|
+
const reports = data.value || [];
|
|
661
|
+
console.log(`\nDirect Reports (${reports.length}):`);
|
|
662
|
+
for (const r of reports)
|
|
663
|
+
console.log(` ${pad(r.displayName || '', 30)} ${r.userPrincipalName}`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Default: show org context
|
|
668
|
+
try {
|
|
669
|
+
const mgr = await graphGet('/me/manager', token);
|
|
670
|
+
console.log(`Manager: ${mgr.displayName}`);
|
|
671
|
+
} catch {}
|
|
672
|
+
const reports = (await graphGet('/me/directReports', token, { $select: 'displayName' })).value || [];
|
|
673
|
+
console.log(`Direct Reports: ${reports.length}`);
|
|
674
|
+
const colleagues = (await graphGet('/me/people', token, { $top: 5 })).value || [];
|
|
675
|
+
console.log('\nFrequent colleagues:');
|
|
676
|
+
for (const p of colleagues) console.log(` ${p.displayName}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ── CLI Parser ────────────────────────────────────────────────────────────────
|
|
680
|
+
function parseArgs(argv) {
|
|
681
|
+
// Flags that take no value (boolean)
|
|
682
|
+
const BOOL = new Set(['json','unread','sites','chats','teamsList','contacts',
|
|
683
|
+
'manager','reports','availability','help']);
|
|
684
|
+
const args = { _: [] };
|
|
685
|
+
let i = 0;
|
|
686
|
+
while (i < argv.length) {
|
|
687
|
+
const a = argv[i];
|
|
688
|
+
if (a.startsWith('--')) {
|
|
689
|
+
const raw = a.slice(2);
|
|
690
|
+
const key = raw.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
691
|
+
if (BOOL.has(key) || i + 1 >= argv.length || argv[i + 1].startsWith('--')) {
|
|
692
|
+
args[key] = true;
|
|
693
|
+
} else {
|
|
694
|
+
args[key] = argv[++i];
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
args._.push(a);
|
|
698
|
+
}
|
|
699
|
+
i++;
|
|
700
|
+
}
|
|
701
|
+
return args;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function printHelp() {
|
|
705
|
+
const name = path.basename(process.argv[1]);
|
|
706
|
+
console.log(`Microsoft Graph API CLI
|
|
707
|
+
Usage: node ${name} <command> [options]
|
|
708
|
+
|
|
709
|
+
Auth:
|
|
710
|
+
login Authenticate (device code flow)
|
|
711
|
+
logout Remove cached credentials
|
|
712
|
+
status Check login status
|
|
713
|
+
|
|
714
|
+
Data:
|
|
715
|
+
me [--json] Your profile
|
|
716
|
+
emails [--limit N] [--unread] [--search Q] [--folder NAME]
|
|
717
|
+
[--read ID] [--send TO --subject S --body B] [--json]
|
|
718
|
+
calendar [--limit N] [--json]
|
|
719
|
+
[--create TITLE --start DT --end DT [--location L] [--timezone TZ]]
|
|
720
|
+
[--availability --start DT --end DT]
|
|
721
|
+
sharepoint [--sites] [--site ID [--path P]] [--download ID [--output FILE]] [--json]
|
|
722
|
+
teams [--chats] [--messages CHAT_ID] [--send MSG --chat-id ID] [--teams-list] [--json]
|
|
723
|
+
onedrive [--path P] [--upload FILE [--dest PATH]] [--download ID [--output FILE]]
|
|
724
|
+
[--info ID] [--json]
|
|
725
|
+
people [--contacts] [--search NAME] [--limit N] [--json]
|
|
726
|
+
org [--manager] [--reports] [--json]
|
|
727
|
+
|
|
728
|
+
Add --json to any command for machine-readable output.
|
|
729
|
+
|
|
730
|
+
Examples:
|
|
731
|
+
node ${name} login
|
|
732
|
+
node ${name} emails --limit 20
|
|
733
|
+
node ${name} emails --send user@corp.com --subject "Hi" --body "Hello"
|
|
734
|
+
node ${name} calendar --create "Standup" --start 2024-03-15T09:00 --end 2024-03-15T09:30
|
|
735
|
+
node ${name} teams --chats
|
|
736
|
+
node ${name} onedrive --upload report.pdf --dest "Documents/report.pdf"
|
|
737
|
+
`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
741
|
+
async function main() {
|
|
742
|
+
const argv = process.argv.slice(2);
|
|
743
|
+
if (!argv.length) { printHelp(); process.exit(0); }
|
|
744
|
+
|
|
745
|
+
const command = argv[0];
|
|
746
|
+
const args = parseArgs(argv.slice(1));
|
|
747
|
+
|
|
748
|
+
const COMMANDS = {
|
|
749
|
+
login: () => cmdLogin(),
|
|
750
|
+
logout: () => cmdLogout(),
|
|
751
|
+
status: () => cmdStatus(),
|
|
752
|
+
me: () => cmdMe(args),
|
|
753
|
+
emails: () => cmdEmails(args),
|
|
754
|
+
calendar: () => cmdCalendar(args),
|
|
755
|
+
sharepoint: () => cmdSharepoint(args),
|
|
756
|
+
teams: () => cmdTeams(args),
|
|
757
|
+
onedrive: () => cmdOnedrive(args),
|
|
758
|
+
people: () => cmdPeople(args),
|
|
759
|
+
org: () => cmdOrg(args),
|
|
760
|
+
help: () => { printHelp(); process.exit(0); },
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
if (!COMMANDS[command]) {
|
|
764
|
+
console.error(`Unknown command: ${command}`);
|
|
765
|
+
printHelp();
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
await COMMANDS[command]();
|
|
771
|
+
} catch (err) {
|
|
772
|
+
if (err.statusCode === 401) {
|
|
773
|
+
console.error(`Error: Authentication expired. Run: node ${path.basename(process.argv[1])} login`);
|
|
774
|
+
} else if (err.statusCode === 403) {
|
|
775
|
+
console.error(`Error: Permission denied (${err.responseUrl || ''})`);
|
|
776
|
+
console.error('You may need additional OAuth scopes.');
|
|
777
|
+
} else if (err.statusCode === 404) {
|
|
778
|
+
console.error(`Error: Resource not found (${err.responseUrl || ''})`);
|
|
779
|
+
} else if (err.statusCode) {
|
|
780
|
+
console.error(`HTTP Error ${err.statusCode}: ${(err.responseBody || '').slice(0, 200)}`);
|
|
781
|
+
} else {
|
|
782
|
+
console.error(`Error: ${err.message}`);
|
|
783
|
+
}
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
main().catch(err => {
|
|
789
|
+
console.error(err.message);
|
|
790
|
+
process.exit(1);
|
|
791
|
+
});
|