outlook-cli 1.2.0 → 1.2.2

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/CLI.md CHANGED
@@ -13,7 +13,9 @@ outlook-cli --help
13
13
  ## Global options
14
14
 
15
15
  - `--json`
16
+ - `--ai`
16
17
  - `--output text|json`
18
+ - `--theme k9s|ocean|mono`
17
19
  - `--plain`
18
20
  - `--no-color`
19
21
  - `--no-animate`
@@ -34,9 +36,20 @@ outlook-cli tools schema send-email
34
36
  outlook-cli auth status
35
37
  outlook-cli auth url
36
38
  outlook-cli auth login --open --start-server --wait --timeout 180
39
+ outlook-cli auth login --open --client-id <id> --client-secret <secret>
40
+ outlook-cli auth server --start
41
+ outlook-cli auth server --status
37
42
  outlook-cli auth logout
38
43
  ```
39
44
 
45
+ ## AI Agents
46
+
47
+ ```bash
48
+ outlook-cli agents guide
49
+ outlook-cli tools list --ai
50
+ outlook-cli call list-emails --args-json '{"folder":"inbox","count":5}' --ai
51
+ ```
52
+
40
53
  ## Email
41
54
 
42
55
  ```bash
@@ -86,4 +99,5 @@ outlook-cli call create-event --args-json '{"subject":"Standup","start":"2026-04
86
99
  outlook-cli doctor
87
100
  outlook-cli update
88
101
  outlook-cli update --run
102
+ outlook-cli mcp-server
89
103
  ```
package/README.md CHANGED
@@ -10,7 +10,7 @@ Production-ready CLI and MCP server for Microsoft Outlook through Microsoft Grap
10
10
 
11
11
  - Global CLI command: `outlook-cli`
12
12
  - One-time OAuth token storage — reused across runs and updates
13
- - Human-friendly commands with rich terminal output, plus machine-friendly `--json` mode
13
+ - Human-friendly commands with rich terminal output, theme presets, and machine-friendly `--json`/`--ai` mode
14
14
  - 19 MCP tools available to Claude and other AI assistants via the shared tool registry
15
15
  - Full email, calendar, folder, and rules management through Microsoft Graph API
16
16
 
@@ -139,12 +139,16 @@ outlook-cli calendar list # list upcoming events
139
139
  | `auth status` | Show current authentication status | `outlook-cli auth status` |
140
140
  | `auth login` | Start authentication flow | `outlook-cli auth login --open --start-server --wait` |
141
141
  | `auth url` | Show the OAuth URL without opening browser | `outlook-cli auth url` |
142
+ | `auth server` | Check/start local OAuth callback server | `outlook-cli auth server --start` |
142
143
  | `auth logout` | Clear stored tokens | `outlook-cli auth logout` |
143
144
 
144
145
  **Flags for `auth login`:**
145
146
  - `--open` — Automatically open the auth URL in your browser
146
147
  - `--start-server` — Start the OAuth callback server automatically
147
148
  - `--wait` — Wait for authentication to complete before returning
149
+ - `--client-id` — Provide Application (client) ID at runtime (optional)
150
+ - `--client-secret` — Provide client secret value at runtime (optional)
151
+ - `--prompt-credentials` — Prompt for missing credentials in interactive terminals
148
152
 
149
153
  **Examples:**
150
154
  ```bash
@@ -159,6 +163,13 @@ outlook-cli auth status
159
163
 
160
164
  # Force re-authentication
161
165
  outlook-cli auth login --open --force
166
+
167
+ # Runtime credentials (useful outside repo where .env is not loaded)
168
+ outlook-cli auth login --open --client-id <id> --client-secret <secret>
169
+
170
+ # Start or check auth callback server
171
+ outlook-cli auth server --start
172
+ outlook-cli auth server --status
162
173
  ```
163
174
 
164
175
  ---
@@ -750,6 +761,24 @@ outlook-cli call list-events --args-json '{"count":10}' --json
750
761
 
751
762
  ---
752
763
 
764
+ ### `agents` — AI Agent Guide
765
+
766
+ Shows best-practice command flow for AI agents (Claude, Codex, VS Code, automation scripts).
767
+
768
+ ```bash
769
+ outlook-cli agents guide
770
+ ```
771
+
772
+ **Agent workflow (recommended):**
773
+ ```bash
774
+ outlook-cli auth status --json
775
+ outlook-cli tools list --json
776
+ outlook-cli tools schema send-email --json
777
+ outlook-cli call list-emails --args-json '{"folder":"inbox","count":5}' --ai
778
+ ```
779
+
780
+ ---
781
+
753
782
  ### `doctor` — Diagnostics
754
783
 
755
784
  Runs a series of diagnostic checks and reports what's working and what needs fixing.
@@ -784,14 +813,22 @@ npm i -g outlook-cli@latest
784
813
  | Flag | Description |
785
814
  |---|---|
786
815
  | `--json` | Output raw JSON instead of human-readable text |
816
+ | `--ai` | Agent-safe alias for JSON mode (suppresses rich UI output) |
817
+ | `--theme k9s|ocean|mono` | Select color theme for text output |
787
818
  | `--plain` | No colors or formatting |
788
819
  | `--no-color` | Disable color only |
789
820
  | `--no-animate` | Disable spinner animations |
790
821
 
822
+ In JSON mode, responses include both:
823
+ - `result` (original MCP tool payload)
824
+ - `structured` (normalized machine-friendly fields like `summary`, `items`, and parsed metadata)
825
+
791
826
  **Examples:**
792
827
  ```bash
793
828
  outlook-cli auth status --json
829
+ outlook-cli tools list --ai
794
830
  outlook-cli email list --count 10 --json
831
+ outlook-cli --theme ocean --help
795
832
  outlook-cli calendar list --plain
796
833
  ```
797
834
 
@@ -50,6 +50,34 @@ function getTokenStorage() {
50
50
  return tokenStorage;
51
51
  }
52
52
 
53
+ function updateAuthConfig(partialConfig = {}) {
54
+ const nextConfig = {
55
+ ...config.AUTH_CONFIG,
56
+ ...partialConfig
57
+ };
58
+
59
+ config.AUTH_CONFIG.clientId = nextConfig.clientId;
60
+ config.AUTH_CONFIG.clientSecret = nextConfig.clientSecret;
61
+ config.AUTH_CONFIG.redirectUri = nextConfig.redirectUri;
62
+ config.AUTH_CONFIG.scopes = nextConfig.scopes;
63
+ config.AUTH_CONFIG.tokenStorePath = nextConfig.tokenStorePath;
64
+ config.AUTH_CONFIG.authServerUrl = nextConfig.authServerUrl;
65
+ config.AUTH_CONFIG.tokenEndpoint = nextConfig.tokenEndpoint;
66
+
67
+ if (tokenStorage) {
68
+ const existingTokens = tokenStorage.tokens;
69
+ tokenStorage = new TokenStorage({
70
+ tokenStorePath: config.AUTH_CONFIG.tokenStorePath,
71
+ clientId: config.AUTH_CONFIG.clientId,
72
+ clientSecret: config.AUTH_CONFIG.clientSecret,
73
+ redirectUri: config.AUTH_CONFIG.redirectUri,
74
+ scopes: config.AUTH_CONFIG.scopes,
75
+ tokenEndpoint: config.AUTH_CONFIG.tokenEndpoint
76
+ });
77
+ tokenStorage.tokens = existingTokens;
78
+ }
79
+ }
80
+
53
81
  /**
54
82
  * Loads authentication tokens from the token file
55
83
  * @returns {object|null} - The loaded tokens or null if not available
@@ -195,5 +223,6 @@ module.exports = {
195
223
  getAccessToken,
196
224
  getValidAccessToken,
197
225
  createTestTokens,
198
- clearTokenCache
226
+ clearTokenCache,
227
+ updateAuthConfig
199
228
  };
package/cli.js CHANGED
@@ -11,6 +11,7 @@ const fs = require('fs');
11
11
  const http = require('http');
12
12
  const https = require('https');
13
13
  const path = require('path');
14
+ const readline = require('readline');
14
15
  const { spawn, spawnSync } = require('child_process');
15
16
  const chalk = require('chalk');
16
17
 
@@ -21,10 +22,51 @@ const { listTools, getTool, invokeTool } = require('./tool-registry');
21
22
 
22
23
  const CLI_BIN_NAME = 'outlook-cli';
23
24
  const SPINNER_FRAMES = ['|', '/', '-', '\\'];
25
+ const THEMES = Object.freeze({
26
+ k9s: {
27
+ title: ['bold', 'cyanBright'],
28
+ section: ['bold', 'greenBright'],
29
+ ok: ['bold', 'greenBright'],
30
+ warn: ['bold', 'yellowBright'],
31
+ err: ['bold', 'redBright'],
32
+ muted: ['gray'],
33
+ accent: ['bold', 'cyan'],
34
+ key: ['bold', 'whiteBright'],
35
+ value: ['cyan'],
36
+ border: ['cyanBright']
37
+ },
38
+ ocean: {
39
+ title: ['bold', 'blueBright'],
40
+ section: ['bold', 'cyanBright'],
41
+ ok: ['bold', 'green'],
42
+ warn: ['bold', 'yellow'],
43
+ err: ['bold', 'redBright'],
44
+ muted: ['gray'],
45
+ accent: ['bold', 'magentaBright'],
46
+ key: ['bold', 'white'],
47
+ value: ['blueBright'],
48
+ border: ['blue']
49
+ },
50
+ mono: {
51
+ title: ['bold', 'white'],
52
+ section: ['bold', 'white'],
53
+ ok: ['bold', 'white'],
54
+ warn: ['bold', 'white'],
55
+ err: ['bold', 'white'],
56
+ muted: ['gray'],
57
+ accent: ['bold', 'white'],
58
+ key: ['bold', 'white'],
59
+ value: ['white'],
60
+ border: ['gray']
61
+ }
62
+ });
63
+ const DEFAULT_THEME_NAME = 'k9s';
24
64
  const UI_STATE = {
25
65
  plain: false,
26
66
  color: false,
27
- animate: false
67
+ animate: false,
68
+ themeName: DEFAULT_THEME_NAME,
69
+ theme: THEMES[DEFAULT_THEME_NAME]
28
70
  };
29
71
 
30
72
  function cliCommand(pathTail = '') {
@@ -206,42 +248,75 @@ function asCsv(value) {
206
248
  }
207
249
 
208
250
  function setUiState(options, outputMode) {
251
+ const aiMode = asBoolean(readOption(options, 'ai', false), false);
209
252
  const plain = asBoolean(readOption(options, 'plain', false), false);
210
253
  const colorOption = readOption(options, 'color');
211
254
  const animateOption = readOption(options, 'animate');
255
+ const requestedTheme = String(readOption(
256
+ options,
257
+ 'theme',
258
+ process.env.OUTLOOK_CLI_THEME || DEFAULT_THEME_NAME
259
+ )).trim().toLowerCase();
260
+ const themeName = THEMES[requestedTheme] ? requestedTheme : DEFAULT_THEME_NAME;
212
261
  const isTextMode = outputMode === 'text';
213
262
  const isInteractive = isTextMode && process.stdout.isTTY;
214
263
 
215
- UI_STATE.plain = plain;
216
- UI_STATE.color = isInteractive && !plain && colorOption !== false;
217
- UI_STATE.animate = isInteractive && !plain && animateOption !== false;
264
+ UI_STATE.plain = plain || aiMode;
265
+ UI_STATE.color = isInteractive && !UI_STATE.plain && colorOption !== false;
266
+ UI_STATE.animate = isInteractive && !UI_STATE.plain && animateOption !== false;
267
+ UI_STATE.themeName = themeName;
268
+ UI_STATE.theme = THEMES[themeName];
218
269
 
219
270
  chalk.level = UI_STATE.color ? Math.max(chalk.level, 1) : 0;
220
271
  }
221
272
 
222
- function tone(text, kind) {
223
- if (!UI_STATE.color) {
273
+ function applyThemeStyle(text, stylePath) {
274
+ if (!UI_STATE.color || !stylePath) {
224
275
  return text;
225
276
  }
226
277
 
227
- switch (kind) {
228
- case 'title':
229
- return chalk.bold.cyan(text);
230
- case 'section':
231
- return chalk.bold.blue(text);
232
- case 'ok':
233
- return chalk.green(text);
234
- case 'warn':
235
- return chalk.yellow(text);
236
- case 'err':
237
- return chalk.red(text);
238
- case 'muted':
239
- return chalk.gray(text);
240
- case 'accent':
241
- return chalk.magenta(text);
242
- default:
278
+ const chain = Array.isArray(stylePath) ? stylePath : [stylePath];
279
+ let painter = chalk;
280
+
281
+ for (const segment of chain) {
282
+ if (!(segment in painter)) {
243
283
  return text;
284
+ }
285
+
286
+ painter = painter[segment];
244
287
  }
288
+
289
+ return painter(text);
290
+ }
291
+
292
+ function terminalWidth() {
293
+ const raw = Number(process.stdout.columns || 100);
294
+ if (!Number.isFinite(raw)) {
295
+ return 100;
296
+ }
297
+
298
+ return Math.max(72, Math.min(120, raw));
299
+ }
300
+
301
+ function padRight(value, width) {
302
+ const text = String(value);
303
+ if (text.length >= width) {
304
+ return text;
305
+ }
306
+
307
+ return `${text}${' '.repeat(width - text.length)}`;
308
+ }
309
+
310
+ function separator(width = terminalWidth()) {
311
+ return '-'.repeat(Math.max(24, width));
312
+ }
313
+
314
+ function formatRow(left, right, leftWidth = 34) {
315
+ return ` ${tone(padRight(left, leftWidth), 'key')} ${tone(right, 'muted')}`;
316
+ }
317
+
318
+ function tone(text, kind) {
319
+ return applyThemeStyle(text, UI_STATE.theme[kind]);
245
320
  }
246
321
 
247
322
  function badge(kind) {
@@ -316,9 +391,242 @@ function formatResultText(result) {
316
391
  return JSON.stringify(result, null, 2);
317
392
  }
318
393
 
394
+ function normalizeLineEndings(value) {
395
+ return String(value || '').replace(/\r\n/g, '\n').trim();
396
+ }
397
+
398
+ function extractTextChunksFromResult(result) {
399
+ if (!result || !Array.isArray(result.content)) {
400
+ return [];
401
+ }
402
+
403
+ return result.content
404
+ .filter((entry) => entry && entry.type === 'text' && typeof entry.text === 'string')
405
+ .map((entry) => normalizeLineEndings(entry.text))
406
+ .filter(Boolean);
407
+ }
408
+
409
+ function firstNonEmptyLine(text) {
410
+ const line = normalizeLineEndings(text)
411
+ .split('\n')
412
+ .map((entry) => entry.trim())
413
+ .find(Boolean);
414
+
415
+ return line || '';
416
+ }
417
+
418
+ function parseNumberedBlocks(text) {
419
+ const normalized = normalizeLineEndings(text);
420
+ const matches = normalized.match(/\d+\.\s[\s\S]*?(?=(?:\n\d+\.\s)|$)/g);
421
+ if (!matches) {
422
+ return [];
423
+ }
424
+
425
+ return matches.map((entry) => entry.trim()).filter(Boolean);
426
+ }
427
+
428
+ function parseEmailListFromText(text) {
429
+ const normalized = normalizeLineEndings(text);
430
+ const body = normalized.replace(/^Found[^\n]*:\n\n?/i, '');
431
+ const blocks = parseNumberedBlocks(body);
432
+
433
+ return blocks.map((block) => {
434
+ const lines = block.split('\n').map((line) => line.trim()).filter(Boolean);
435
+ const headline = lines[0] || '';
436
+ const subjectLine = lines.find((line) => line.startsWith('Subject:')) || '';
437
+ const idLine = lines.find((line) => line.startsWith('ID:')) || '';
438
+ const match = headline.match(/^(\d+)\.\s(?:\[UNREAD\]\s)?(.+?)\s-\sFrom:\s(.+?)\s\((.+)\)$/);
439
+
440
+ return {
441
+ index: match ? Number(match[1]) : null,
442
+ unread: headline.includes('[UNREAD]'),
443
+ receivedAt: match ? match[2] : null,
444
+ from: match ? { name: match[3], email: match[4] } : null,
445
+ subject: subjectLine ? subjectLine.replace(/^Subject:\s*/, '') : null,
446
+ id: idLine ? idLine.replace(/^ID:\s*/, '') : null,
447
+ raw: block
448
+ };
449
+ });
450
+ }
451
+
452
+ function parseEventListFromText(text) {
453
+ const normalized = normalizeLineEndings(text);
454
+ const body = normalized.replace(/^Found[^\n]*:\n\n?/i, '');
455
+ const blocks = parseNumberedBlocks(body);
456
+
457
+ return blocks.map((block) => {
458
+ const lines = block.split('\n').map((line) => line.trim()).filter(Boolean);
459
+ const headline = lines[0] || '';
460
+ const startLine = lines.find((line) => line.startsWith('Start:')) || '';
461
+ const endLine = lines.find((line) => line.startsWith('End:')) || '';
462
+ const summaryLine = lines.find((line) => line.startsWith('Summary:')) || '';
463
+ const idLine = lines.find((line) => line.startsWith('ID:')) || '';
464
+ const match = headline.match(/^(\d+)\.\s(.+?)\s-\sLocation:\s(.+)$/);
465
+
466
+ return {
467
+ index: match ? Number(match[1]) : null,
468
+ subject: match ? match[2] : null,
469
+ location: match ? match[3] : null,
470
+ start: startLine ? startLine.replace(/^Start:\s*/, '') : null,
471
+ end: endLine ? endLine.replace(/^End:\s*/, '') : null,
472
+ summary: summaryLine ? summaryLine.replace(/^Summary:\s*/, '') : null,
473
+ id: idLine ? idLine.replace(/^ID:\s*/, '') : null,
474
+ raw: block
475
+ };
476
+ });
477
+ }
478
+
479
+ function parseFolderListFromText(text) {
480
+ const normalized = normalizeLineEndings(text);
481
+ if (normalized.startsWith('Folder Hierarchy:')) {
482
+ const lines = normalized
483
+ .replace(/^Folder Hierarchy:\n\n?/i, '')
484
+ .split('\n')
485
+ .filter(Boolean);
486
+
487
+ return lines.map((line) => {
488
+ const leadingSpaces = line.match(/^\s*/);
489
+ const level = leadingSpaces ? Math.floor(leadingSpaces[0].length / 2) : 0;
490
+ return {
491
+ level,
492
+ name: line.trim(),
493
+ raw: line
494
+ };
495
+ });
496
+ }
497
+
498
+ const body = normalized.replace(/^Found[^\n]*:\n\n?/i, '');
499
+ return body
500
+ .split('\n')
501
+ .map((line) => line.trim())
502
+ .filter(Boolean)
503
+ .map((line, index) => ({
504
+ index: index + 1,
505
+ name: line,
506
+ raw: line
507
+ }));
508
+ }
509
+
510
+ function parseRuleListFromText(text) {
511
+ const normalized = normalizeLineEndings(text);
512
+ const body = normalized.replace(/^Found[^\n]*:\n\n?/i, '');
513
+ const blocks = parseNumberedBlocks(body);
514
+
515
+ return blocks.map((block) => {
516
+ const lines = block.split('\n').map((line) => line.trim()).filter(Boolean);
517
+ const headline = lines[0] || '';
518
+ const conditionsLine = lines.find((line) => line.startsWith('Conditions:')) || '';
519
+ const actionsLine = lines.find((line) => line.startsWith('Actions:')) || '';
520
+ const match = headline.match(/^(\d+)\.\s(.+?)(?:\s-\sSequence:\s(.+))?$/);
521
+
522
+ return {
523
+ index: match ? Number(match[1]) : null,
524
+ name: match ? match[2] : null,
525
+ sequence: match && match[3] ? match[3] : null,
526
+ conditions: conditionsLine ? conditionsLine.replace(/^Conditions:\s*/, '') : null,
527
+ actions: actionsLine ? actionsLine.replace(/^Actions:\s*/, '') : null,
528
+ raw: block
529
+ };
530
+ });
531
+ }
532
+
533
+ function parseReadEmailFromText(text) {
534
+ const normalized = normalizeLineEndings(text);
535
+ const sections = normalized.split(/\n\n+/);
536
+ const headerSection = sections.shift() || '';
537
+ const headerLines = headerSection.split('\n').map((line) => line.trim()).filter(Boolean);
538
+ const headers = {};
539
+
540
+ headerLines.forEach((line) => {
541
+ const index = line.indexOf(':');
542
+ if (index > 0) {
543
+ const key = line.slice(0, index).trim();
544
+ const value = line.slice(index + 1).trim();
545
+ headers[key] = value;
546
+ }
547
+ });
548
+
549
+ return {
550
+ headers,
551
+ body: sections.join('\n\n').trim(),
552
+ raw: normalized
553
+ };
554
+ }
555
+
556
+ function normalizeToolResult(toolName, args, result) {
557
+ const textChunks = extractTextChunksFromResult(result);
558
+ const text = textChunks.join('\n\n');
559
+ const normalized = {
560
+ tool: toolName,
561
+ args: args || {},
562
+ summary: firstNonEmptyLine(text),
563
+ textChunks
564
+ };
565
+
566
+ switch (toolName) {
567
+ case 'list-emails':
568
+ case 'search-emails':
569
+ normalized.kind = 'email-list';
570
+ normalized.items = parseEmailListFromText(text);
571
+ normalized.count = normalized.items.length;
572
+ return normalized;
573
+
574
+ case 'list-events':
575
+ normalized.kind = 'event-list';
576
+ normalized.items = parseEventListFromText(text);
577
+ normalized.count = normalized.items.length;
578
+ return normalized;
579
+
580
+ case 'list-folders':
581
+ normalized.kind = 'folder-list';
582
+ normalized.items = parseFolderListFromText(text);
583
+ normalized.count = normalized.items.length;
584
+ return normalized;
585
+
586
+ case 'list-rules':
587
+ normalized.kind = 'rule-list';
588
+ normalized.items = parseRuleListFromText(text);
589
+ normalized.count = normalized.items.length;
590
+ return normalized;
591
+
592
+ case 'read-email':
593
+ normalized.kind = 'email-detail';
594
+ normalized.record = parseReadEmailFromText(text);
595
+ return normalized;
596
+
597
+ default:
598
+ normalized.kind = 'text';
599
+ normalized.lines = normalizeLineEndings(text).split('\n').filter(Boolean);
600
+ return normalized;
601
+ }
602
+ }
603
+
604
+ function buildStructuredPayload(payload) {
605
+ const response = { ...payload };
606
+
607
+ if (payload.tool && payload.result) {
608
+ response.structured = normalizeToolResult(payload.tool, payload.args, payload.result);
609
+ return response;
610
+ }
611
+
612
+ response.structured = {
613
+ command: payload.command || null,
614
+ summary: typeof payload.message === 'string' ? payload.message : null,
615
+ data: payload.data || null
616
+ };
617
+
618
+ return response;
619
+ }
620
+
621
+ function printTextBlock(title, body) {
622
+ process.stdout.write(`${tone(title, 'section')}\n`);
623
+ process.stdout.write(`${tone(separator(32), 'border')}\n`);
624
+ process.stdout.write(`${body}\n`);
625
+ }
626
+
319
627
  function printSuccess(outputMode, payload) {
320
628
  if (outputMode === 'json') {
321
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
629
+ process.stdout.write(`${JSON.stringify(buildStructuredPayload(payload), null, 2)}\n`);
322
630
  return;
323
631
  }
324
632
 
@@ -327,11 +635,11 @@ function printSuccess(outputMode, payload) {
327
635
  }
328
636
 
329
637
  if (payload.result) {
330
- process.stdout.write(`${tone('Result:', 'section')}\n${formatResultText(payload.result)}\n`);
638
+ printTextBlock('Result', formatResultText(payload.result));
331
639
  }
332
640
 
333
641
  if (payload.data && !payload.result) {
334
- process.stdout.write(`${tone('Details:', 'section')}\n${JSON.stringify(payload.data, null, 2)}\n`);
642
+ printTextBlock('Details', JSON.stringify(payload.data, null, 2));
335
643
  }
336
644
  }
337
645
 
@@ -350,11 +658,16 @@ function printError(outputMode, error) {
350
658
 
351
659
  process.stderr.write(`${badge('err')} ${error.message}\n`);
352
660
  if (error instanceof UsageError) {
353
- process.stderr.write(`${tone(`Run '${cliCommand('help')}' to see valid usage.`, 'muted')}\n`);
661
+ process.stderr.write(`${tone(separator(42), 'border')}\n`);
662
+ process.stderr.write(`${tone(formatRow('help', `Run '${cliCommand('help')}' to see valid usage.`, 14), 'muted')}\n`);
354
663
  }
355
664
  }
356
665
 
357
666
  function buildOutputMode(options) {
667
+ if (asBoolean(readOption(options, 'ai', false), false)) {
668
+ return 'json';
669
+ }
670
+
358
671
  const outputOption = readOption(options, 'output');
359
672
  if (outputOption === 'json') {
360
673
  return 'json';
@@ -421,41 +734,49 @@ async function callTool(toolName, args, outputMode, commandLabel) {
421
734
  function printUsage() {
422
735
  const usage = [
423
736
  `${tone(CLI_BIN_NAME, 'title')} ${tone(`v${pkg.version}`, 'muted')}`,
737
+ tone(`Theme: ${UI_STATE.themeName} | AI mode: --ai`, 'muted'),
738
+ tone(separator(), 'border'),
424
739
  '',
425
- tone('Global install:', 'section'),
426
- ` npm i -g ${pkg.name}`,
740
+ tone('Install', 'section'),
741
+ formatRow('global', `npm i -g ${pkg.name}`, 18),
742
+ formatRow('local', 'npm install', 18),
427
743
  '',
428
- tone('Usage:', 'section'),
429
- ` ${cliCommand('<command> [subcommand] [options]')}`,
744
+ tone('Usage', 'section'),
745
+ ` ${tone(cliCommand('<command> [subcommand] [options]'), 'accent')}`,
430
746
  '',
431
- tone('Global options:', 'section'),
432
- ' --json Output machine-readable JSON',
433
- ' --output text|json Explicit output mode',
434
- ' --plain Disable rich colors and animations',
435
- ' --no-color Disable colors explicitly',
436
- ' --no-animate Disable spinner animations',
437
- ' --help Show help',
438
- ' --version Show version',
747
+ tone('Global options', 'section'),
748
+ formatRow('--json', 'Machine-readable JSON output'),
749
+ formatRow('--ai', 'Agent-safe alias for JSON mode (no spinners/colors)'),
750
+ formatRow('--output text|json', 'Explicit output mode'),
751
+ formatRow('--theme k9s|ocean|mono', 'Theme preset for text output'),
752
+ formatRow('--plain', 'Disable rich styling and animations'),
753
+ formatRow('--no-color', 'Disable colors explicitly'),
754
+ formatRow('--no-animate', 'Disable spinner animations'),
755
+ formatRow('--help', 'Show help'),
756
+ formatRow('--version', 'Show version'),
439
757
  '',
440
- tone('Core commands:', 'section'),
441
- ' commands Show all command groups and tools',
442
- ' tools list',
443
- ' tools schema <tool-name>',
444
- ' call <tool-name> [--args-json "{...}"] [--arg key=value]',
445
- ' auth status|url|login|logout',
446
- ' update [--run] [--to latest|x.y.z]',
447
- ' doctor',
758
+ tone('Core commands', 'section'),
759
+ formatRow('commands', 'Show command groups and available tools'),
760
+ formatRow('tools list', 'List all tool names and descriptions'),
761
+ formatRow('tools schema <tool-name>', 'Show JSON schema for one tool'),
762
+ formatRow('call <tool-name> [--args-json] [--arg]', 'Invoke any tool directly'),
763
+ formatRow('auth status|url|login|logout|server', 'Authentication and OAuth server control'),
764
+ formatRow('agents guide', 'AI agent quick-start and orchestration tips'),
765
+ formatRow('doctor', 'Run diagnostics and environment checks'),
766
+ formatRow('update [--run] [--to latest|x.y.z]', 'Check or apply global update'),
767
+ formatRow('mcp-server', 'Run stdio MCP server (for Claude/Codex/VS Code)'),
448
768
  '',
449
- tone('Human-friendly command groups:', 'section'),
450
- ' email list|search|read|send|mark-read',
451
- ' calendar list|create|decline|cancel|delete',
452
- ' folder list|create|move',
453
- ' rule list|create|sequence',
769
+ tone('Command groups', 'section'),
770
+ formatRow('email', 'list|search|read|send|mark-read'),
771
+ formatRow('calendar', 'list|create|decline|cancel|delete'),
772
+ formatRow('folder', 'list|create|move'),
773
+ formatRow('rule', 'list|create|sequence'),
454
774
  '',
455
- tone('Examples:', 'section'),
775
+ tone('Examples', 'section'),
456
776
  ` ${cliCommand('auth login --open --start-server --wait --timeout 180')}`,
457
- ` ${cliCommand('email list --folder inbox --count 15')}`,
458
- ` ${cliCommand('call search-emails --arg query=invoice --arg unreadOnly=true --json')}`,
777
+ ` ${cliCommand('auth login --open --client-id <id> --client-secret <secret>')}`,
778
+ ` ${cliCommand('agents guide --json')}`,
779
+ ` ${cliCommand("call list-emails --args-json '{\"folder\":\"inbox\",\"count\":5}' --ai")}`,
459
780
  ` ${cliCommand('tools schema send-email')}`,
460
781
  ` ${cliCommand('update --run')}`
461
782
  ];
@@ -471,19 +792,89 @@ function getAuthUrl() {
471
792
  return `${config.AUTH_CONFIG.authServerUrl}/auth?client_id=${config.AUTH_CONFIG.clientId}`;
472
793
  }
473
794
 
795
+ function applyRuntimeAuthConfig(partialConfig = {}) {
796
+ const nextConfig = {
797
+ ...config.AUTH_CONFIG,
798
+ ...partialConfig
799
+ };
800
+
801
+ if (nextConfig.clientId) {
802
+ process.env.OUTLOOK_CLIENT_ID = nextConfig.clientId;
803
+ process.env.MS_CLIENT_ID = nextConfig.clientId;
804
+ }
805
+
806
+ if (nextConfig.clientSecret) {
807
+ process.env.OUTLOOK_CLIENT_SECRET = nextConfig.clientSecret;
808
+ process.env.MS_CLIENT_SECRET = nextConfig.clientSecret;
809
+ }
810
+
811
+ tokenManager.updateAuthConfig(nextConfig);
812
+ }
813
+
814
+ function promptLine(promptText) {
815
+ return new Promise((resolve) => {
816
+ const rl = readline.createInterface({
817
+ input: process.stdin,
818
+ output: process.stdout
819
+ });
820
+
821
+ rl.question(promptText, (answer) => {
822
+ rl.close();
823
+ resolve(String(answer || '').trim());
824
+ });
825
+ });
826
+ }
827
+
828
+ async function ensureAuthCredentials(options, outputMode) {
829
+ let clientId = readOption(options, 'clientId', config.AUTH_CONFIG.clientId);
830
+ let clientSecret = readOption(options, 'clientSecret', config.AUTH_CONFIG.clientSecret);
831
+ const promptCredentials = asBoolean(readOption(options, 'promptCredentials', true), true);
832
+ const canPrompt = outputMode === 'text' && process.stdin.isTTY && process.stdout.isTTY;
833
+
834
+ if ((!clientId || !clientSecret) && promptCredentials && canPrompt) {
835
+ if (outputMode === 'text') {
836
+ process.stdout.write(`${badge('warn')} Missing Microsoft app credentials. Provide values now or pass --client-id/--client-secret.\n`);
837
+ }
838
+
839
+ if (!clientId) {
840
+ clientId = await promptLine('MS_CLIENT_ID (or OUTLOOK_CLIENT_ID): ');
841
+ }
842
+
843
+ if (!clientSecret) {
844
+ clientSecret = await promptLine('MS_CLIENT_SECRET (or OUTLOOK_CLIENT_SECRET): ');
845
+ }
846
+ }
847
+
848
+ if (clientId || clientSecret) {
849
+ applyRuntimeAuthConfig({
850
+ clientId,
851
+ clientSecret
852
+ });
853
+ }
854
+
855
+ if (!config.AUTH_CONFIG.clientId) {
856
+ throw new CliError('Client ID is missing. Set OUTLOOK_CLIENT_ID or MS_CLIENT_ID, or pass --client-id.', 1);
857
+ }
858
+
859
+ if (!config.AUTH_CONFIG.clientSecret) {
860
+ throw new CliError('Client secret is missing. Set OUTLOOK_CLIENT_SECRET or MS_CLIENT_SECRET, or pass --client-secret.', 1);
861
+ }
862
+ }
863
+
474
864
  function buildCommandCatalog() {
475
865
  const toolCatalog = listTools();
476
866
 
477
867
  return {
478
868
  commandGroups: {
479
- auth: ['status', 'url', 'login', 'logout'],
869
+ auth: ['status', 'url', 'login', 'logout', 'server'],
480
870
  email: ['list', 'search', 'read', 'send', 'mark-read'],
481
871
  calendar: ['list', 'create', 'decline', 'cancel', 'delete'],
482
872
  folder: ['list', 'create', 'move'],
483
873
  rule: ['list', 'create', 'sequence'],
874
+ agents: ['guide'],
484
875
  tools: ['list', 'schema'],
485
876
  generic: ['call'],
486
- system: ['doctor', 'update', 'version', 'help']
877
+ system: ['doctor', 'update', 'version', 'help', 'mcp-server']
487
878
  },
488
879
  tools: toolCatalog
489
880
  };
@@ -498,18 +889,21 @@ function printCommandCatalog(outputMode) {
498
889
  data: catalog,
499
890
  message: outputMode === 'text'
500
891
  ? [
501
- `Command groups:`,
502
- `- auth: ${catalog.commandGroups.auth.join(', ')}`,
503
- `- email: ${catalog.commandGroups.email.join(', ')}`,
504
- `- calendar: ${catalog.commandGroups.calendar.join(', ')}`,
505
- `- folder: ${catalog.commandGroups.folder.join(', ')}`,
506
- `- rule: ${catalog.commandGroups.rule.join(', ')}`,
507
- `- tools: ${catalog.commandGroups.tools.join(', ')}`,
508
- `- generic: ${catalog.commandGroups.generic.join(', ')}`,
509
- `- system: ${catalog.commandGroups.system.join(', ')}`,
892
+ tone('Command groups', 'section'),
893
+ tone(separator(48), 'border'),
894
+ formatRow('auth', catalog.commandGroups.auth.join(', '), 14),
895
+ formatRow('email', catalog.commandGroups.email.join(', '), 14),
896
+ formatRow('calendar', catalog.commandGroups.calendar.join(', '), 14),
897
+ formatRow('folder', catalog.commandGroups.folder.join(', '), 14),
898
+ formatRow('rule', catalog.commandGroups.rule.join(', '), 14),
899
+ formatRow('agents', catalog.commandGroups.agents.join(', '), 14),
900
+ formatRow('tools', catalog.commandGroups.tools.join(', '), 14),
901
+ formatRow('generic', catalog.commandGroups.generic.join(', '), 14),
902
+ formatRow('system', catalog.commandGroups.system.join(', '), 14),
510
903
  '',
511
- `Available MCP tools (${catalog.tools.length}):`,
512
- ...catalog.tools.map((tool) => `- ${tool.name}`)
904
+ tone(`Available MCP tools (${catalog.tools.length})`, 'section'),
905
+ tone(separator(48), 'border'),
906
+ ...catalog.tools.map((tool) => formatRow(tool.name, tool.description || 'No description', 28))
513
907
  ].join('\n')
514
908
  : undefined
515
909
  });
@@ -594,20 +988,27 @@ function openInBrowser(url) {
594
988
  spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
595
989
  }
596
990
 
597
- function startAuthServer(outputMode) {
991
+ function startAuthServer(outputMode, runtime = {}) {
598
992
  const scriptPath = path.join(__dirname, 'outlook-auth-server.js');
599
993
 
600
994
  if (!fs.existsSync(scriptPath)) {
601
995
  throw new CliError(`Auth server script not found: ${scriptPath}`, 1);
602
996
  }
603
997
 
998
+ const detached = asBoolean(runtime.detached, false);
999
+
604
1000
  const child = spawn(process.execPath, [scriptPath], {
605
1001
  cwd: __dirname,
606
1002
  windowsHide: true,
607
1003
  stdio: 'ignore',
608
- env: process.env
1004
+ env: process.env,
1005
+ detached
609
1006
  });
610
1007
 
1008
+ if (detached) {
1009
+ child.unref();
1010
+ }
1011
+
611
1012
  child.on('error', (error) => {
612
1013
  if (outputMode === 'text') {
613
1014
  process.stderr.write(`Auth server failed to start: ${error.message}\n`);
@@ -617,6 +1018,53 @@ function startAuthServer(outputMode) {
617
1018
  return child;
618
1019
  }
619
1020
 
1021
+ async function handleAuthServerCommand(options, outputMode) {
1022
+ const shouldStart = asBoolean(readOption(options, 'start', false), false);
1023
+ const shouldStatus = asBoolean(readOption(options, 'status', !shouldStart), !shouldStart);
1024
+
1025
+ if (!shouldStart && !shouldStatus) {
1026
+ throw new UsageError(`Usage: ${cliCommand('auth server [--status] [--start]')}`);
1027
+ }
1028
+
1029
+ if (shouldStart) {
1030
+ const isAlreadyRunning = await httpProbe(config.AUTH_CONFIG.authServerUrl);
1031
+ if (!isAlreadyRunning) {
1032
+ startAuthServer(outputMode, { detached: true });
1033
+ const ready = await waitForProbe(config.AUTH_CONFIG.authServerUrl, 10000);
1034
+ if (!ready) {
1035
+ throw new CliError('Auth server did not become ready on time.', 1);
1036
+ }
1037
+ }
1038
+
1039
+ printSuccess(outputMode, {
1040
+ ok: true,
1041
+ command: 'auth server --start',
1042
+ data: {
1043
+ authServerUrl: config.AUTH_CONFIG.authServerUrl,
1044
+ alreadyRunning: isAlreadyRunning
1045
+ },
1046
+ message: isAlreadyRunning
1047
+ ? `Auth server already running at ${config.AUTH_CONFIG.authServerUrl}.`
1048
+ : `Auth server started at ${config.AUTH_CONFIG.authServerUrl}.`
1049
+ });
1050
+
1051
+ return;
1052
+ }
1053
+
1054
+ const reachable = await httpProbe(config.AUTH_CONFIG.authServerUrl);
1055
+ printSuccess(outputMode, {
1056
+ ok: true,
1057
+ command: 'auth server --status',
1058
+ data: {
1059
+ authServerUrl: config.AUTH_CONFIG.authServerUrl,
1060
+ running: reachable
1061
+ },
1062
+ message: reachable
1063
+ ? `Auth server is reachable at ${config.AUTH_CONFIG.authServerUrl}.`
1064
+ : `Auth server is not reachable at ${config.AUTH_CONFIG.authServerUrl}.`
1065
+ });
1066
+ }
1067
+
620
1068
  async function waitForValidToken(timeoutMs, onTick) {
621
1069
  const start = Date.now();
622
1070
  let attempts = 0;
@@ -665,6 +1113,11 @@ async function handleAuthCommand(action, options, outputMode) {
665
1113
  }
666
1114
 
667
1115
  case 'url': {
1116
+ const providedClientId = readOption(options, 'clientId');
1117
+ if (providedClientId) {
1118
+ applyRuntimeAuthConfig({ clientId: String(providedClientId).trim() });
1119
+ }
1120
+
668
1121
  const authUrl = getAuthUrl();
669
1122
  printSuccess(outputMode, {
670
1123
  ok: true,
@@ -687,6 +1140,8 @@ async function handleAuthCommand(action, options, outputMode) {
687
1140
  }
688
1141
 
689
1142
  case 'login': {
1143
+ await ensureAuthCredentials(options, outputMode);
1144
+
690
1145
  const force = asBoolean(readOption(options, 'force', false));
691
1146
  const shouldOpen = asBoolean(readOption(options, 'open', true), true);
692
1147
  const shouldStartServer = asBoolean(readOption(options, 'startServer', true), true);
@@ -718,7 +1173,7 @@ async function handleAuthCommand(action, options, outputMode) {
718
1173
  const runningBefore = await httpProbe(config.AUTH_CONFIG.authServerUrl);
719
1174
  if (!runningBefore && shouldStartServer) {
720
1175
  startSpinner = createSpinner('Starting auth server...', outputMode).start();
721
- startedServer = startAuthServer(outputMode);
1176
+ startedServer = startAuthServer(outputMode, { detached: false });
722
1177
  const ready = await waitForProbe(
723
1178
  config.AUTH_CONFIG.authServerUrl,
724
1179
  10000,
@@ -812,8 +1267,13 @@ async function handleAuthCommand(action, options, outputMode) {
812
1267
  return;
813
1268
  }
814
1269
 
1270
+ case 'server': {
1271
+ await handleAuthServerCommand(options, outputMode);
1272
+ return;
1273
+ }
1274
+
815
1275
  default:
816
- throw new UsageError('Unknown auth command. Use: auth status|url|login|logout');
1276
+ throw new UsageError('Unknown auth command. Use: auth status|url|login|logout|server');
817
1277
  }
818
1278
  }
819
1279
 
@@ -1152,6 +1612,56 @@ async function handleRuleCommand(action, options, outputMode) {
1152
1612
  }
1153
1613
  }
1154
1614
 
1615
+ function startMcpServerProcess() {
1616
+ // eslint-disable-next-line global-require
1617
+ require('./index');
1618
+ }
1619
+
1620
+ async function handleAgentsCommand(action, outputMode) {
1621
+ const subcommand = action || 'guide';
1622
+ if (subcommand !== 'guide') {
1623
+ throw new UsageError('Unknown agents command. Use: agents guide');
1624
+ }
1625
+
1626
+ const tools = listTools();
1627
+ const guide = {
1628
+ totalTools: tools.length,
1629
+ toolNames: tools.map((tool) => tool.name),
1630
+ mcpServerCommand: `${CLI_BIN_NAME} mcp-server`,
1631
+ recommendedFlow: [
1632
+ `${CLI_BIN_NAME} auth status --json`,
1633
+ `${CLI_BIN_NAME} tools list --json`,
1634
+ `${CLI_BIN_NAME} tools schema <tool-name> --json`,
1635
+ `${CLI_BIN_NAME} call <tool-name> --args-json '{"...":"..."}' --json`
1636
+ ],
1637
+ safetyNotes: [
1638
+ 'Prefer --json or --ai for agent workflows.',
1639
+ 'Use tools schema before call for strict argument validation.',
1640
+ 'Use auth status before calling Microsoft Graph tools.'
1641
+ ]
1642
+ };
1643
+
1644
+ printSuccess(outputMode, {
1645
+ ok: true,
1646
+ command: 'agents guide',
1647
+ data: guide,
1648
+ message: outputMode === 'text'
1649
+ ? [
1650
+ tone('Agent guide', 'section'),
1651
+ tone(separator(48), 'border'),
1652
+ formatRow('mcp server', guide.mcpServerCommand, 18),
1653
+ formatRow('available tools', String(guide.totalTools), 18),
1654
+ '',
1655
+ tone('Recommended workflow', 'section'),
1656
+ ...guide.recommendedFlow.map((line) => ` ${tone(line, 'accent')}`),
1657
+ '',
1658
+ tone('Best practices', 'section'),
1659
+ ...guide.safetyNotes.map((note) => formatRow('note', note, 18))
1660
+ ].join('\n')
1661
+ : undefined
1662
+ });
1663
+ }
1664
+
1155
1665
  async function handleDoctorCommand(outputMode) {
1156
1666
  const tokenPath = config.AUTH_CONFIG.tokenStorePath;
1157
1667
  const tokenExists = fs.existsSync(tokenPath);
@@ -1307,6 +1817,11 @@ async function run() {
1307
1817
  await handleGenericCall(positional.slice(1), options, outputMode);
1308
1818
  return;
1309
1819
 
1820
+ case 'agents':
1821
+ case 'agent':
1822
+ await handleAgentsCommand(action, outputMode);
1823
+ return;
1824
+
1310
1825
  case 'auth':
1311
1826
  await handleAuthCommand(action, options, outputMode);
1312
1827
  return;
@@ -1335,6 +1850,10 @@ async function run() {
1335
1850
  await handleUpdateCommand(options, outputMode);
1336
1851
  return;
1337
1852
 
1853
+ case 'mcp-server':
1854
+ startMcpServerProcess();
1855
+ return;
1856
+
1338
1857
  default:
1339
1858
  throw new UsageError(`Unknown command: ${command}. Run '${cliCommand('help')}' for usage.`);
1340
1859
  }
package/docs/REFERENCE.md CHANGED
@@ -11,7 +11,8 @@ This document is the complete, publish-ready reference for:
11
11
  - CLI binary: `outlook-cli`
12
12
  - Output mode:
13
13
  - `text` (default)
14
- - `json` (with `--json` or `--output json`)
14
+ - `json` (with `--json`, `--ai`, or `--output json`)
15
+ - JSON responses include `structured` for normalized, machine-friendly fields while preserving raw `result`.
15
16
  - Option key normalization:
16
17
  - `--event-id` and `--eventId` map to the same internal key.
17
18
  - `--start-server` and `--startServer` are equivalent.
@@ -24,7 +25,9 @@ This document is the complete, publish-ready reference for:
24
25
  | Option | Type | Required | Default | Description |
25
26
  |---|---|---:|---|---|
26
27
  | `--json` | boolean | No | `false` | Forces JSON output for automation and agents. |
28
+ | `--ai` | boolean | No | `false` | Alias for JSON-first agent mode (no rich UI noise). |
27
29
  | `--output` | `text` \| `json` | No | `text` | Explicit output mode override. |
30
+ | `--theme` | `k9s` \| `ocean` \| `mono` | No | `k9s` | Text-mode color theme preset. |
28
31
  | `--plain` | boolean | No | `false` | Disables rich color and animation UI output. |
29
32
  | `--no-color` | boolean | No | `false` | Disables terminal colors. |
30
33
  | `--no-animate` | boolean | No | `false` | Disables loading/waiting spinner animation. |
@@ -40,11 +43,13 @@ This document is the complete, publish-ready reference for:
40
43
  | `tools list` | Lists all tools with descriptions and schemas. |
41
44
  | `tools schema <tool-name>` | Prints schema for one tool. |
42
45
  | `call <tool-name>` | Calls any registered MCP tool directly. |
46
+ | `agents guide` | Prints AI-agent workflow guidance and best practices. |
43
47
  | `auth ...` | Authentication command group. |
44
48
  | `email ...` | Email command group. |
45
49
  | `calendar ...` | Calendar command group. |
46
50
  | `folder ...` | Folder command group. |
47
51
  | `rule ...` | Rules command group. |
52
+ | `mcp-server` | Starts the stdio MCP server (same behavior as `node index.js`). |
48
53
  | `doctor` | Environment and auth diagnostics. |
49
54
  | `update` | Prints or runs global npm update command. |
50
55
  | `help` | Same as `--help`. |
@@ -118,6 +123,18 @@ Examples:
118
123
  ```bash
119
124
  outlook-cli call list-emails --args-json '{"count":5}'
120
125
  outlook-cli call search-emails --arg query=invoice --arg unreadOnly=true --json
126
+ outlook-cli call send-email --args-json '{"to":"a@example.com","subject":"Hi","body":"Hello"}' --ai
127
+ ```
128
+
129
+ ### agents guide
130
+
131
+ Prints AI-agent command patterns and orchestration best practices.
132
+
133
+ Usage:
134
+
135
+ ```bash
136
+ outlook-cli agents guide
137
+ outlook-cli agents guide --json
121
138
  ```
122
139
 
123
140
  ### auth status
@@ -137,7 +154,7 @@ Prints the Microsoft OAuth URL generated from current config.
137
154
  Usage:
138
155
 
139
156
  ```bash
140
- outlook-cli auth url
157
+ outlook-cli auth url [--client-id <id>]
141
158
  ```
142
159
 
143
160
  ### auth login
@@ -159,6 +176,27 @@ Options:
159
176
  | `--start-server` | boolean | No | `true` | Starts local auth server if not already running. |
160
177
  | `--wait` | boolean | No | `true` | Waits for token completion before returning. |
161
178
  | `--timeout` | number (seconds) | No | `180` | Max wait time when `--wait` is true. Min effective timeout is 5s. |
179
+ | `--client-id` | string | No | env/config | Runtime override for client ID. |
180
+ | `--client-secret` | string | No | env/config | Runtime override for client secret value. |
181
+ | `--prompt-credentials` | boolean | No | `true` | Prompt for missing credentials in interactive terminals. |
182
+
183
+ ### auth server
184
+
185
+ Checks or starts the local OAuth callback server.
186
+
187
+ Usage:
188
+
189
+ ```bash
190
+ outlook-cli auth server --status
191
+ outlook-cli auth server --start
192
+ ```
193
+
194
+ Options:
195
+
196
+ | Option | Type | Required | Default | Description |
197
+ |---|---|---:|---|---|
198
+ | `--status` | boolean | No | `true` when `--start` is not passed | Prints reachability status only. |
199
+ | `--start` | boolean | No | `false` | Starts callback server in background if not running. |
162
200
 
163
201
  ### auth logout
164
202
 
package/email/search.js CHANGED
@@ -6,6 +6,12 @@ const { callGraphAPI } = require('../utils/graph-api');
6
6
  const { ensureAuthenticated } = require('../auth');
7
7
  const { resolveFolderPath } = require('./folder-utils');
8
8
 
9
+ function debugLog(...args) {
10
+ if (config.DEBUG_LOGS) {
11
+ console.error(...args);
12
+ }
13
+ }
14
+
9
15
  /**
10
16
  * Search emails handler
11
17
  * @param {object} args - Tool arguments
@@ -27,7 +33,7 @@ async function handleSearchEmails(args) {
27
33
 
28
34
  // Resolve the folder path
29
35
  const endpoint = await resolveFolderPath(accessToken, folder);
30
- console.error(`Using endpoint: ${endpoint} for folder: ${folder}`);
36
+ debugLog(`Using endpoint: ${endpoint} for folder: ${folder}`);
31
37
 
32
38
  // Execute progressive search
33
39
  const response = await progressiveSearch(
@@ -76,16 +82,16 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
76
82
  // 1. Try combined search (most specific)
77
83
  try {
78
84
  const params = buildSearchParams(searchTerms, filterTerms, count);
79
- console.error("Attempting combined search with params:", params);
85
+ debugLog('Attempting combined search with params:', params);
80
86
  searchAttempts.push("combined-search");
81
87
 
82
88
  const response = await callGraphAPI(accessToken, 'GET', endpoint, null, params);
83
89
  if (response.value && response.value.length > 0) {
84
- console.error(`Combined search successful: found ${response.value.length} results`);
90
+ debugLog(`Combined search successful: found ${response.value.length} results`);
85
91
  return response;
86
92
  }
87
93
  } catch (error) {
88
- console.error(`Combined search failed: ${error.message}`);
94
+ debugLog(`Combined search failed: ${error.message}`);
89
95
  }
90
96
 
91
97
  // 2. Try each search term individually, starting with most specific
@@ -94,7 +100,7 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
94
100
  for (const term of searchPriority) {
95
101
  if (searchTerms[term]) {
96
102
  try {
97
- console.error(`Attempting search with only ${term}: "${searchTerms[term]}"`);
103
+ debugLog(`Attempting search with only ${term}: "${searchTerms[term]}"`);
98
104
  searchAttempts.push(`single-term-${term}`);
99
105
 
100
106
  // For single term search, only use $search with that term
@@ -118,11 +124,11 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
118
124
 
119
125
  const response = await callGraphAPI(accessToken, 'GET', endpoint, null, simplifiedParams);
120
126
  if (response.value && response.value.length > 0) {
121
- console.error(`Search with ${term} successful: found ${response.value.length} results`);
127
+ debugLog(`Search with ${term} successful: found ${response.value.length} results`);
122
128
  return response;
123
129
  }
124
130
  } catch (error) {
125
- console.error(`Search with ${term} failed: ${error.message}`);
131
+ debugLog(`Search with ${term} failed: ${error.message}`);
126
132
  }
127
133
  }
128
134
  }
@@ -130,7 +136,7 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
130
136
  // 3. Try with only boolean filters
131
137
  if (filterTerms.hasAttachments === true || filterTerms.unreadOnly === true) {
132
138
  try {
133
- console.error("Attempting search with only boolean filters");
139
+ debugLog('Attempting search with only boolean filters');
134
140
  searchAttempts.push("boolean-filters-only");
135
141
 
136
142
  const filterOnlyParams = {
@@ -143,15 +149,15 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
143
149
  addBooleanFilters(filterOnlyParams, filterTerms);
144
150
 
145
151
  const response = await callGraphAPI(accessToken, 'GET', endpoint, null, filterOnlyParams);
146
- console.error(`Boolean filter search found ${response.value?.length || 0} results`);
152
+ debugLog(`Boolean filter search found ${response.value?.length || 0} results`);
147
153
  return response;
148
154
  } catch (error) {
149
- console.error(`Boolean filter search failed: ${error.message}`);
155
+ debugLog(`Boolean filter search failed: ${error.message}`);
150
156
  }
151
157
  }
152
158
 
153
159
  // 4. Final fallback: just get recent emails
154
- console.error("All search strategies failed, falling back to recent emails");
160
+ debugLog('All search strategies failed, falling back to recent emails');
155
161
  searchAttempts.push("recent-emails");
156
162
 
157
163
  const basicParams = {
@@ -161,7 +167,7 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
161
167
  };
162
168
 
163
169
  const response = await callGraphAPI(accessToken, 'GET', endpoint, null, basicParams);
164
- console.error(`Fallback to recent emails found ${response.value?.length || 0} results`);
170
+ debugLog(`Fallback to recent emails found ${response.value?.length || 0} results`);
165
171
 
166
172
  // Add a note to the response about the search attempts
167
173
  response._searchInfo = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "outlook-cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Production-ready Outlook CLI with optional MCP server mode powered by Microsoft Graph API",
5
5
  "keywords": [
6
6
  "outlook-cli",
package/rules/index.js CHANGED
@@ -1,14 +1,11 @@
1
1
  /**
2
2
  * Email rules management module for Outlook MCP server
3
3
  */
4
- const handleListRules = require('./list');
4
+ const { handleListRules, getInboxRules } = require('./list');
5
5
  const handleCreateRule = require('./create');
6
6
  const { ensureAuthenticated } = require('../auth');
7
7
  const { callGraphAPI } = require('../utils/graph-api');
8
8
 
9
- // Import getInboxRules for the edit sequence tool
10
- const { getInboxRules } = require('./list');
11
-
12
9
  /**
13
10
  * Edit rule sequence handler
14
11
  * @param {object} args - Tool arguments
@@ -5,6 +5,12 @@ const https = require('https');
5
5
  const config = require('../config');
6
6
  const mockData = require('./mock-data');
7
7
 
8
+ function debugLog(...args) {
9
+ if (config.DEBUG_LOGS) {
10
+ console.error(...args);
11
+ }
12
+ }
13
+
8
14
  /**
9
15
  * Makes a request to the Microsoft Graph API
10
16
  * @param {string} accessToken - The access token for authentication
@@ -17,12 +23,12 @@ const mockData = require('./mock-data');
17
23
  async function callGraphAPI(accessToken, method, path, data = null, queryParams = {}) {
18
24
  // For test tokens, we'll simulate the API call
19
25
  if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
20
- console.error(`TEST MODE: Simulating ${method} ${path} API call`);
26
+ debugLog(`TEST MODE: Simulating ${method} ${path} API call`);
21
27
  return mockData.simulateGraphAPIResponse(method, path, data, queryParams);
22
28
  }
23
29
 
24
30
  try {
25
- console.error(`Making real API call: ${method} ${path}`);
31
+ debugLog(`Making real API call: ${method} ${path}`);
26
32
 
27
33
  // Encode path segments properly
28
34
  const encodedPath = path.split('/')
@@ -59,11 +65,11 @@ async function callGraphAPI(accessToken, method, path, data = null, queryParams
59
65
  queryString = '?' + queryString;
60
66
  }
61
67
 
62
- console.error(`Query string: ${queryString}`);
68
+ debugLog(`Query string: ${queryString}`);
63
69
  }
64
70
 
65
71
  const url = `${config.GRAPH_API_ENDPOINT}${encodedPath}${queryString}`;
66
- console.error(`Full URL: ${url}`);
72
+ debugLog(`Full URL: ${url}`);
67
73
 
68
74
  return new Promise((resolve, reject) => {
69
75
  const options = {
@@ -110,7 +116,7 @@ async function callGraphAPI(accessToken, method, path, data = null, queryParams
110
116
  req.end();
111
117
  });
112
118
  } catch (error) {
113
- console.error('Error calling Graph API:', error);
119
+ debugLog('Error calling Graph API:', error);
114
120
  throw error;
115
121
  }
116
122
  }