msteams-mcp 0.2.0 → 0.3.1

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.
@@ -5,6 +5,30 @@
5
5
  */
6
6
  import { readSessionState, readTokenCache, writeTokenCache, clearTokenCache, getTeamsOrigin, } from './session-store.js';
7
7
  import { parseJwtProfile } from '../utils/parsers.js';
8
+ /**
9
+ * Decodes a JWT token's payload without verifying the signature.
10
+ * Returns null if the token is invalid.
11
+ */
12
+ function decodeJwtPayload(token) {
13
+ try {
14
+ const parts = token.split('.');
15
+ if (parts.length < 2)
16
+ return null;
17
+ return JSON.parse(Buffer.from(parts[1], 'base64').toString());
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * Gets the expiry date from a JWT token.
25
+ */
26
+ function getJwtExpiry(token) {
27
+ const payload = decodeJwtPayload(token);
28
+ if (!payload?.exp || typeof payload.exp !== 'number')
29
+ return null;
30
+ return new Date(payload.exp * 1000);
31
+ }
8
32
  /**
9
33
  * Extracts the Substrate search token from session state.
10
34
  */
@@ -22,10 +46,8 @@ export function extractSubstrateToken(state) {
22
46
  const token = val.secret;
23
47
  if (!token || typeof token !== 'string')
24
48
  continue;
25
- const parts = token.split('.');
26
- if (parts.length === 3) {
27
- const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
28
- const expiry = new Date(payload.exp * 1000);
49
+ const expiry = getJwtExpiry(token);
50
+ if (expiry) {
29
51
  return { token, expiry };
30
52
  }
31
53
  }
@@ -107,13 +129,12 @@ export function extractTeamsToken(state) {
107
129
  const secret = val.secret;
108
130
  if (typeof secret !== 'string' || !secret.startsWith('ey'))
109
131
  continue;
110
- const parts = secret.split('.');
111
- if (parts.length !== 3)
132
+ const payload = decodeJwtPayload(secret);
133
+ if (!payload?.exp || typeof payload.exp !== 'number')
112
134
  continue;
113
- const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
114
135
  const tokenExpiry = new Date(payload.exp * 1000);
115
136
  // Extract user MRI from any token
116
- if (payload.oid && !userMri) {
137
+ if (typeof payload.oid === 'string' && !userMri) {
117
138
  userMri = `8:orgid:${payload.oid}`;
118
139
  }
119
140
  // Prefer chatsvcagg.teams.microsoft.com token
@@ -139,17 +160,9 @@ export function extractTeamsToken(state) {
139
160
  if (!userMri) {
140
161
  const substrateInfo = extractSubstrateToken(sessionState);
141
162
  if (substrateInfo) {
142
- try {
143
- const parts = substrateInfo.token.split('.');
144
- if (parts.length === 3) {
145
- const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
146
- if (payload.oid) {
147
- userMri = `8:orgid:${payload.oid}`;
148
- }
149
- }
150
- }
151
- catch {
152
- // Ignore
163
+ const payload = decodeJwtPayload(substrateInfo.token);
164
+ if (typeof payload?.oid === 'string') {
165
+ userMri = `8:orgid:${payload.oid}`;
153
166
  }
154
167
  }
155
168
  }
@@ -185,32 +198,16 @@ export function extractMessageAuth(state) {
185
198
  }
186
199
  // Get userMri from skypeToken payload
187
200
  if (skypeToken) {
188
- try {
189
- const parts = skypeToken.split('.');
190
- if (parts.length >= 2) {
191
- const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
192
- if (payload.skypeid) {
193
- userMri = payload.skypeid;
194
- }
195
- }
196
- }
197
- catch {
198
- // Not a JWT format, that's fine
201
+ const payload = decodeJwtPayload(skypeToken);
202
+ if (typeof payload?.skypeid === 'string') {
203
+ userMri = payload.skypeid;
199
204
  }
200
205
  }
201
206
  // Fallback to extracting userMri from authToken
202
207
  if (!userMri && authToken) {
203
- try {
204
- const parts = authToken.split('.');
205
- if (parts.length === 3) {
206
- const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
207
- if (payload.oid) {
208
- userMri = `8:orgid:${payload.oid}`;
209
- }
210
- }
211
- }
212
- catch {
213
- // Ignore
208
+ const payload = decodeJwtPayload(authToken);
209
+ if (typeof payload?.oid === 'string') {
210
+ userMri = `8:orgid:${payload.oid}`;
214
211
  }
215
212
  }
216
213
  if (skypeToken && authToken && userMri) {
@@ -252,7 +249,6 @@ export function getUserProfile(state) {
252
249
  const teamsOrigin = getTeamsOrigin(sessionState);
253
250
  if (!teamsOrigin)
254
251
  return null;
255
- // Look through localStorage for any JWT with user info
256
252
  for (const item of teamsOrigin.localStorage) {
257
253
  try {
258
254
  const val = JSON.parse(item.value);
@@ -260,13 +256,11 @@ export function getUserProfile(state) {
260
256
  continue;
261
257
  if (!val.secret.startsWith('ey'))
262
258
  continue;
263
- const parts = val.secret.split('.');
264
- if (parts.length !== 3)
265
- continue;
266
- const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
267
- const profile = parseJwtProfile(payload);
268
- if (profile) {
269
- return profile;
259
+ const payload = decodeJwtPayload(val.secret);
260
+ if (payload) {
261
+ const profile = parseJwtProfile(payload);
262
+ if (profile)
263
+ return profile;
270
264
  }
271
265
  }
272
266
  catch {
@@ -302,17 +296,9 @@ export function getUserDisplayName(state) {
302
296
  // Try to get from token
303
297
  const teamsToken = extractTeamsToken(sessionState);
304
298
  if (teamsToken) {
305
- try {
306
- const parts = teamsToken.token.split('.');
307
- if (parts.length === 3) {
308
- const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
309
- if (payload.name)
310
- return payload.name;
311
- }
312
- }
313
- catch {
314
- // Ignore
315
- }
299
+ const payload = decodeJwtPayload(teamsToken.token);
300
+ if (typeof payload?.name === 'string')
301
+ return payload.name;
316
302
  }
317
303
  return null;
318
304
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Token refresh via headless browser.
3
+ *
4
+ * Teams uses SPA OAuth2 which restricts refresh tokens to browser-based CORS
5
+ * requests. We open a headless browser with saved session state, let MSAL
6
+ * silently refresh tokens, then save the updated state. Seamless to the user.
7
+ */
8
+ import { type Result } from '../types/result.js';
9
+ /** Result of a successful token refresh. */
10
+ export interface TokenRefreshResult {
11
+ /** New token expiry time. */
12
+ newExpiry: Date;
13
+ /** Previous expiry time (for comparison). */
14
+ previousExpiry: Date;
15
+ /** Minutes gained by refresh. */
16
+ minutesGained: number;
17
+ /** Whether a refresh was actually needed (token was close to expiry). */
18
+ refreshNeeded: boolean;
19
+ }
20
+ /**
21
+ * Refreshes tokens by opening a headless browser with saved session state.
22
+ * MSAL refreshes tokens automatically when Teams loads; we just need to
23
+ * trigger that and save the updated state.
24
+ */
25
+ export declare function refreshTokensViaBrowser(): Promise<Result<TokenRefreshResult>>;
26
+ /**
27
+ * Checks if tokens need refreshing (less than threshold time remaining).
28
+ *
29
+ * @returns true if tokens should be refreshed proactively
30
+ */
31
+ export declare function shouldRefreshToken(): boolean;
32
+ /**
33
+ * Checks if tokens are completely expired.
34
+ *
35
+ * @returns true if tokens have expired and cannot be used
36
+ */
37
+ export declare function isTokenExpired(): boolean;
38
+ /**
39
+ * Attempts to proactively refresh tokens if they're approaching expiry.
40
+ *
41
+ * This is a convenience function that checks if refresh is needed and
42
+ * performs it if necessary. Safe to call on any operation - it only
43
+ * opens a browser when actually needed.
44
+ *
45
+ * @returns The refresh result if refresh was attempted, null if not needed
46
+ */
47
+ export declare function refreshIfNeeded(): Promise<Result<TokenRefreshResult> | null>;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Token refresh via headless browser.
3
+ *
4
+ * Teams uses SPA OAuth2 which restricts refresh tokens to browser-based CORS
5
+ * requests. We open a headless browser with saved session state, let MSAL
6
+ * silently refresh tokens, then save the updated state. Seamless to the user.
7
+ */
8
+ import { TOKEN_REFRESH_THRESHOLD_MS, MSAL_TOKEN_DELAY_MS } from '../constants.js';
9
+ import { ErrorCode, createError } from '../types/errors.js';
10
+ import { ok, err } from '../types/result.js';
11
+ import { extractSubstrateToken, clearTokenCache, } from './token-extractor.js';
12
+ import { hasSessionState, isSessionLikelyExpired, } from './session-store.js';
13
+ /**
14
+ * Refreshes tokens by opening a headless browser with saved session state.
15
+ * MSAL refreshes tokens automatically when Teams loads; we just need to
16
+ * trigger that and save the updated state.
17
+ */
18
+ export async function refreshTokensViaBrowser() {
19
+ // Check we have a session to work with
20
+ if (!hasSessionState()) {
21
+ return err(createError(ErrorCode.AUTH_REQUIRED, 'No session state available. Please run teams_login to authenticate.', { suggestions: ['Call teams_login to authenticate'] }));
22
+ }
23
+ if (isSessionLikelyExpired()) {
24
+ return err(createError(ErrorCode.AUTH_EXPIRED, 'Session is too old and likely expired. Please re-authenticate.', { suggestions: ['Call teams_login to re-authenticate'] }));
25
+ }
26
+ // Get current token expiry for comparison
27
+ const beforeToken = extractSubstrateToken();
28
+ if (!beforeToken) {
29
+ return err(createError(ErrorCode.AUTH_REQUIRED, 'No token found in session. Please run teams_login to authenticate.', { suggestions: ['Call teams_login to authenticate'] }));
30
+ }
31
+ const previousExpiry = beforeToken.expiry;
32
+ // Import browser functions dynamically to avoid circular dependencies
33
+ const { createBrowserContext, closeBrowser } = await import('../browser/context.js');
34
+ let manager = null;
35
+ try {
36
+ // Open headless browser with saved session
37
+ manager = await createBrowserContext({ headless: true });
38
+ // Navigate to Teams - this triggers MSAL to check/refresh tokens
39
+ await manager.page.goto('https://teams.microsoft.com', {
40
+ waitUntil: 'domcontentloaded',
41
+ });
42
+ // Wait for MSAL to refresh tokens in the background
43
+ // The token storage happens asynchronously after page load
44
+ await manager.page.waitForTimeout(MSAL_TOKEN_DELAY_MS);
45
+ // Close browser and save session state (this captures the refreshed tokens)
46
+ await closeBrowser(manager, true);
47
+ manager = null;
48
+ // Clear our token cache to force re-extraction from the new session
49
+ clearTokenCache();
50
+ // Extract the new token to verify we still have valid tokens
51
+ const afterToken = extractSubstrateToken();
52
+ if (!afterToken) {
53
+ return err(createError(ErrorCode.AUTH_EXPIRED, 'Token refresh failed - no token found after refresh attempt.', { suggestions: ['Call teams_login to re-authenticate'] }));
54
+ }
55
+ const newExpiry = afterToken.expiry;
56
+ const minutesGained = Math.round((newExpiry.getTime() - previousExpiry.getTime()) / 1000 / 60);
57
+ // Check if the token was close to expiry and needed refresh
58
+ const wasCloseToExpiry = previousExpiry.getTime() - Date.now() < TOKEN_REFRESH_THRESHOLD_MS;
59
+ // If we needed a refresh but didn't get one, that's an error
60
+ if (wasCloseToExpiry && newExpiry.getTime() <= previousExpiry.getTime()) {
61
+ return err(createError(ErrorCode.AUTH_EXPIRED, 'Token was not refreshed despite being close to expiry. Session may need re-authentication.', { suggestions: ['Call teams_login to re-authenticate'] }));
62
+ }
63
+ return ok({
64
+ newExpiry,
65
+ previousExpiry,
66
+ minutesGained,
67
+ refreshNeeded: wasCloseToExpiry,
68
+ });
69
+ }
70
+ catch (error) {
71
+ // Clean up browser if still open
72
+ if (manager) {
73
+ try {
74
+ await closeBrowser(manager, false);
75
+ }
76
+ catch {
77
+ // Ignore cleanup errors
78
+ }
79
+ }
80
+ const message = error instanceof Error ? error.message : 'Unknown error';
81
+ return err(createError(ErrorCode.UNKNOWN, `Token refresh via browser failed: ${message}`, { suggestions: ['Call teams_login to re-authenticate'] }));
82
+ }
83
+ }
84
+ /**
85
+ * Checks if tokens need refreshing (less than threshold time remaining).
86
+ *
87
+ * @returns true if tokens should be refreshed proactively
88
+ */
89
+ export function shouldRefreshToken() {
90
+ const substrate = extractSubstrateToken();
91
+ if (!substrate)
92
+ return false;
93
+ const timeRemaining = substrate.expiry.getTime() - Date.now();
94
+ return timeRemaining > 0 && timeRemaining < TOKEN_REFRESH_THRESHOLD_MS;
95
+ }
96
+ /**
97
+ * Checks if tokens are completely expired.
98
+ *
99
+ * @returns true if tokens have expired and cannot be used
100
+ */
101
+ export function isTokenExpired() {
102
+ const substrate = extractSubstrateToken();
103
+ if (!substrate)
104
+ return true;
105
+ return substrate.expiry.getTime() <= Date.now();
106
+ }
107
+ /**
108
+ * Attempts to proactively refresh tokens if they're approaching expiry.
109
+ *
110
+ * This is a convenience function that checks if refresh is needed and
111
+ * performs it if necessary. Safe to call on any operation - it only
112
+ * opens a browser when actually needed.
113
+ *
114
+ * @returns The refresh result if refresh was attempted, null if not needed
115
+ */
116
+ export async function refreshIfNeeded() {
117
+ if (!shouldRefreshToken()) {
118
+ return null;
119
+ }
120
+ return refreshTokensViaBrowser();
121
+ }
@@ -52,3 +52,13 @@ export declare const RETRY_BASE_DELAY_MS = 1000;
52
52
  export declare const RETRY_MAX_DELAY_MS = 10000;
53
53
  /** Self-chat (notes) conversation ID. */
54
54
  export declare const SELF_CHAT_ID = "48:notes";
55
+ /** Activity feed (notifications) conversation ID. */
56
+ export declare const NOTIFICATIONS_ID = "48:notifications";
57
+ /** Default limit for activity feed items. */
58
+ export declare const DEFAULT_ACTIVITY_LIMIT = 50;
59
+ /** Maximum limit for activity feed items. */
60
+ export declare const MAX_ACTIVITY_LIMIT = 200;
61
+ /** Maximum conversations to check when aggregating unread status. */
62
+ export declare const MAX_UNREAD_AGGREGATE_CHECK = 20;
63
+ /** Threshold for proactive token refresh (10 minutes before expiry). */
64
+ export declare const TOKEN_REFRESH_THRESHOLD_MS: number;
package/dist/constants.js CHANGED
@@ -70,3 +70,22 @@ export const RETRY_MAX_DELAY_MS = 10000;
70
70
  // ─────────────────────────────────────────────────────────────────────────────
71
71
  /** Self-chat (notes) conversation ID. */
72
72
  export const SELF_CHAT_ID = '48:notes';
73
+ /** Activity feed (notifications) conversation ID. */
74
+ export const NOTIFICATIONS_ID = '48:notifications';
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+ // Activity Feed
77
+ // ─────────────────────────────────────────────────────────────────────────────
78
+ /** Default limit for activity feed items. */
79
+ export const DEFAULT_ACTIVITY_LIMIT = 50;
80
+ /** Maximum limit for activity feed items. */
81
+ export const MAX_ACTIVITY_LIMIT = 200;
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // Unread Status
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ /** Maximum conversations to check when aggregating unread status. */
86
+ export const MAX_UNREAD_AGGREGATE_CHECK = 20;
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // Token Refresh
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ /** Threshold for proactive token refresh (10 minutes before expiry). */
91
+ export const TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
package/dist/index.js CHANGED
File without changes
package/dist/test/cli.js CHANGED
@@ -13,6 +13,7 @@ import { createBrowserContext, closeBrowser } from '../browser/context.js';
13
13
  import { ensureAuthenticated, forceNewLogin } from '../browser/auth.js';
14
14
  import { hasSessionState, getSessionAge, clearSessionState, } from '../auth/session-store.js';
15
15
  import { hasValidSubstrateToken, getSubstrateTokenStatus, extractMessageAuth, getUserProfile, clearTokenCache, } from '../auth/token-extractor.js';
16
+ import { refreshTokensViaBrowser } from '../auth/token-refresh.js';
16
17
  import { searchMessages } from '../api/substrate-api.js';
17
18
  import { sendMessage, sendNoteToSelf } from '../api/chatsvc-api.js';
18
19
  function parseArgs() {
@@ -64,6 +65,7 @@ Commands:
64
65
  me Get current user profile (email, name, Teams ID)
65
66
  login Log in to Teams (opens browser)
66
67
  login --force Force new login (clears existing session)
68
+ refresh Test OAuth token refresh (shows before/after status)
67
69
  help Show this help message
68
70
 
69
71
  Options:
@@ -84,17 +86,27 @@ Examples:
84
86
  npm run cli -- search "query" --from 25
85
87
  npm run cli -- send "Test message to myself"
86
88
  npm run cli -- login --force
89
+ npm run cli -- refresh
87
90
  `);
88
91
  }
89
92
  async function commandStatus(flags) {
90
93
  const hasSession = hasSessionState();
91
94
  const sessionAge = getSessionAge();
92
- const tokenStatus = getSubstrateTokenStatus();
95
+ const substrateStatus = getSubstrateTokenStatus();
96
+ const messageAuth = extractMessageAuth();
97
+ // Check if refresh will trigger soon (within 10 minutes)
98
+ const REFRESH_THRESHOLD_MINS = 10;
99
+ const willRefreshSoon = substrateStatus.minutesRemaining !== undefined &&
100
+ substrateStatus.minutesRemaining <= REFRESH_THRESHOLD_MINS;
93
101
  const status = {
94
- directApi: {
95
- available: tokenStatus.hasToken,
96
- expiresAt: tokenStatus.expiresAt,
97
- minutesRemaining: tokenStatus.minutesRemaining,
102
+ search: {
103
+ available: substrateStatus.hasToken,
104
+ expiresAt: substrateStatus.expiresAt,
105
+ minutesRemaining: substrateStatus.minutesRemaining,
106
+ willAutoRefresh: willRefreshSoon,
107
+ },
108
+ messaging: {
109
+ available: !!messageAuth,
98
110
  },
99
111
  session: {
100
112
  exists: hasSession,
@@ -106,20 +118,44 @@ async function commandStatus(flags) {
106
118
  console.log(JSON.stringify(status, null, 2));
107
119
  }
108
120
  else {
109
- console.log('\nšŸ“Š Status\n');
110
- if (status.directApi.available) {
111
- console.log(`Direct API: āœ… Available (${status.directApi.minutesRemaining} min remaining)`);
121
+ console.log('\nšŸ“Š Token Status\n');
122
+ // Search token (Substrate)
123
+ console.log('Search API (Substrate):');
124
+ if (status.search.available) {
125
+ console.log(` Status: āœ… Valid`);
126
+ console.log(` Expires: ${status.search.expiresAt}`);
127
+ console.log(` Remaining: ${status.search.minutesRemaining} minutes`);
128
+ if (status.search.willAutoRefresh) {
129
+ console.log(` ⚔ Auto-refresh will trigger (< ${REFRESH_THRESHOLD_MINS} min remaining)`);
130
+ }
131
+ }
132
+ else {
133
+ console.log(' Status: āŒ No valid token');
134
+ }
135
+ // Messaging token (Skype)
136
+ console.log('\nMessaging API (Skype):');
137
+ if (status.messaging.available) {
138
+ console.log(' Status: āœ… Valid');
112
139
  }
113
140
  else {
114
- console.log('Direct API: āŒ No valid token (browser login required)');
141
+ console.log(' Status: āŒ No valid token');
115
142
  }
116
- console.log(`Session exists: ${status.session.exists ? 'āœ… Yes' : 'āŒ No'}`);
143
+ // Session info
144
+ console.log('\nSession:');
145
+ console.log(` Exists: ${status.session.exists ? 'āœ… Yes' : 'āŒ No'}`);
117
146
  if (status.session.ageHours !== null) {
118
- console.log(`Session age: ${status.session.ageHours} hours`);
147
+ console.log(` Age: ${status.session.ageHours} hours`);
119
148
  if (status.session.likelyExpired) {
120
- console.log('āš ļø Session may be expired');
149
+ console.log(' āš ļø Session may be expired (>12 hours old)');
121
150
  }
122
151
  }
152
+ // Action hint
153
+ if (!status.search.available || !status.messaging.available) {
154
+ console.log('\nšŸ’” Run: npm run cli -- login');
155
+ }
156
+ else if (status.search.willAutoRefresh) {
157
+ console.log('\nšŸ’” Run: npm run cli -- refresh (to test auto-refresh)');
158
+ }
123
159
  }
124
160
  }
125
161
  async function commandSearch(query, flags, options) {
@@ -267,6 +303,79 @@ async function commandMe(flags) {
267
303
  }
268
304
  }
269
305
  }
306
+ async function commandRefresh(flags) {
307
+ const asJson = flags.has('json');
308
+ // Show current status
309
+ const beforeStatus = getSubstrateTokenStatus();
310
+ if (!asJson) {
311
+ console.log('\nšŸ”„ Token Refresh Test\n');
312
+ console.log('Before refresh:');
313
+ if (beforeStatus.hasToken) {
314
+ console.log(` Token valid: āœ… Yes`);
315
+ console.log(` Expires at: ${beforeStatus.expiresAt}`);
316
+ console.log(` Minutes remaining: ${beforeStatus.minutesRemaining}`);
317
+ }
318
+ else {
319
+ console.log(` Token valid: āŒ No`);
320
+ }
321
+ console.log('\nOpening headless browser to refresh tokens...');
322
+ }
323
+ // Attempt refresh via headless browser
324
+ const result = await refreshTokensViaBrowser();
325
+ if (!result.ok) {
326
+ if (asJson) {
327
+ console.log(JSON.stringify({
328
+ success: false,
329
+ error: result.error.message,
330
+ code: result.error.code,
331
+ before: beforeStatus,
332
+ }, null, 2));
333
+ }
334
+ else {
335
+ console.log(`\nāŒ Refresh failed: ${result.error.message}`);
336
+ if (result.error.code) {
337
+ console.log(` Error code: ${result.error.code}`);
338
+ }
339
+ if (result.error.suggestions) {
340
+ console.log(` Suggestions: ${result.error.suggestions.join(', ')}`);
341
+ }
342
+ }
343
+ process.exit(1);
344
+ }
345
+ // Show new status
346
+ const afterStatus = getSubstrateTokenStatus();
347
+ if (asJson) {
348
+ console.log(JSON.stringify({
349
+ success: true,
350
+ before: beforeStatus,
351
+ after: afterStatus,
352
+ refreshResult: {
353
+ previousExpiry: result.value.previousExpiry.toISOString(),
354
+ newExpiry: result.value.newExpiry.toISOString(),
355
+ minutesGained: result.value.minutesGained,
356
+ refreshNeeded: result.value.refreshNeeded,
357
+ },
358
+ }, null, 2));
359
+ }
360
+ else {
361
+ if (result.value.refreshNeeded) {
362
+ console.log('\nāœ… Token refreshed!\n');
363
+ console.log('After refresh:');
364
+ console.log(` Token valid: āœ… Yes`);
365
+ console.log(` Expires at: ${afterStatus.expiresAt}`);
366
+ console.log(` Minutes remaining: ${afterStatus.minutesRemaining}`);
367
+ console.log(` Time gained: +${result.value.minutesGained} minutes`);
368
+ }
369
+ else {
370
+ console.log('\nāœ… Token is still valid (no refresh needed)\n');
371
+ console.log('Current token:');
372
+ console.log(` Expires at: ${afterStatus.expiresAt}`);
373
+ console.log(` Minutes remaining: ${afterStatus.minutesRemaining}`);
374
+ console.log('\n Note: MSAL only refreshes tokens when they\'re close to expiry.');
375
+ console.log(' Proactive refresh will trigger when <10 minutes remain.');
376
+ }
377
+ }
378
+ }
270
379
  async function commandLogin(flags) {
271
380
  const force = flags.has('force');
272
381
  if (force) {
@@ -311,6 +420,9 @@ async function main() {
311
420
  case 'me':
312
421
  await commandMe(flags);
313
422
  break;
423
+ case 'refresh':
424
+ await commandRefresh(flags);
425
+ break;
314
426
  case 'login':
315
427
  await commandLogin(flags);
316
428
  break;
@@ -33,6 +33,9 @@ const SHORTCUTS = {
33
33
  contacts: { tool: 'teams_get_frequent_contacts' },
34
34
  channel: { tool: 'teams_find_channel', primaryArg: 'query' },
35
35
  chat: { tool: 'teams_get_chat', primaryArg: 'userId' },
36
+ unread: { tool: 'teams_get_unread' },
37
+ markread: { tool: 'teams_mark_read' },
38
+ activity: { tool: 'teams_get_activity' },
36
39
  };
37
40
  // Map CLI flags to tool parameter names
38
41
  const FLAG_MAPPINGS = {
@@ -48,6 +51,7 @@ const FLAG_MAPPINGS = {
48
51
  '--force': 'forceNew',
49
52
  '--user': 'userId',
50
53
  '--userId': 'userId',
54
+ '--markRead': 'markRead',
51
55
  };
52
56
  function parseArgs() {
53
57
  const args = process.argv.slice(2);
@@ -251,6 +255,9 @@ function prettyPrintResponse(response, toolName) {
251
255
  else if (response.messages && Array.isArray(response.messages)) {
252
256
  printMessagesList(response.messages);
253
257
  }
258
+ else if (response.activities && Array.isArray(response.activities)) {
259
+ printActivityList(response.activities);
260
+ }
254
261
  else if (response.contacts && Array.isArray(response.contacts)) {
255
262
  printContactsList(response.contacts);
256
263
  }
@@ -340,6 +347,30 @@ function printContactsList(contacts) {
340
347
  log('');
341
348
  }
342
349
  }
350
+ function printActivityList(activities) {
351
+ log(`Found ${activities.length} activity items:\n`);
352
+ const typeIcons = {
353
+ mention: 'šŸ“£',
354
+ reaction: 'šŸ‘',
355
+ reply: 'šŸ’¬',
356
+ message: 'šŸ“',
357
+ unknown: 'ā“',
358
+ };
359
+ for (const a of activities) {
360
+ const activity = a;
361
+ const type = activity.type || 'unknown';
362
+ const icon = typeIcons[type] || 'ā“';
363
+ const sender = activity.sender?.displayName || 'Unknown';
364
+ const time = activity.timestamp ? new Date(activity.timestamp).toLocaleString() : '';
365
+ const topic = activity.topic ? ` in "${activity.topic}"` : '';
366
+ const preview = String(activity.content ?? '').substring(0, 80).replace(/\n/g, ' ');
367
+ log(`${icon} [${type}] ${sender}${topic} - ${time}`);
368
+ log(` ${preview}${String(activity.content ?? '').length > 80 ? '...' : ''}`);
369
+ if (activity.conversationId)
370
+ log(` ConversationId: ${activity.conversationId}`);
371
+ log('');
372
+ }
373
+ }
343
374
  function printProfile(profile) {
344
375
  log('šŸ‘¤ Profile:\n');
345
376
  for (const [key, value] of Object.entries(profile)) {