outlook-cli 1.2.1 → 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
 
package/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Production-oriented local CLI for outlook-cli.
4
4
  *
@@ -22,10 +22,51 @@ const { listTools, getTool, invokeTool } = require('./tool-registry');
22
22
 
23
23
  const CLI_BIN_NAME = 'outlook-cli';
24
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';
25
64
  const UI_STATE = {
26
65
  plain: false,
27
66
  color: false,
28
- animate: false
67
+ animate: false,
68
+ themeName: DEFAULT_THEME_NAME,
69
+ theme: THEMES[DEFAULT_THEME_NAME]
29
70
  };
30
71
 
31
72
  function cliCommand(pathTail = '') {
@@ -207,42 +248,75 @@ function asCsv(value) {
207
248
  }
208
249
 
209
250
  function setUiState(options, outputMode) {
251
+ const aiMode = asBoolean(readOption(options, 'ai', false), false);
210
252
  const plain = asBoolean(readOption(options, 'plain', false), false);
211
253
  const colorOption = readOption(options, 'color');
212
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;
213
261
  const isTextMode = outputMode === 'text';
214
262
  const isInteractive = isTextMode && process.stdout.isTTY;
215
263
 
216
- UI_STATE.plain = plain;
217
- UI_STATE.color = isInteractive && !plain && colorOption !== false;
218
- 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];
219
269
 
220
270
  chalk.level = UI_STATE.color ? Math.max(chalk.level, 1) : 0;
221
271
  }
222
272
 
223
- function tone(text, kind) {
224
- if (!UI_STATE.color) {
273
+ function applyThemeStyle(text, stylePath) {
274
+ if (!UI_STATE.color || !stylePath) {
225
275
  return text;
226
276
  }
227
277
 
228
- switch (kind) {
229
- case 'title':
230
- return chalk.bold.cyan(text);
231
- case 'section':
232
- return chalk.bold.blue(text);
233
- case 'ok':
234
- return chalk.green(text);
235
- case 'warn':
236
- return chalk.yellow(text);
237
- case 'err':
238
- return chalk.red(text);
239
- case 'muted':
240
- return chalk.gray(text);
241
- case 'accent':
242
- return chalk.magenta(text);
243
- 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)) {
244
283
  return text;
284
+ }
285
+
286
+ painter = painter[segment];
245
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]);
246
320
  }
247
321
 
248
322
  function badge(kind) {
@@ -317,9 +391,242 @@ function formatResultText(result) {
317
391
  return JSON.stringify(result, null, 2);
318
392
  }
319
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
+
320
627
  function printSuccess(outputMode, payload) {
321
628
  if (outputMode === 'json') {
322
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
629
+ process.stdout.write(`${JSON.stringify(buildStructuredPayload(payload), null, 2)}\n`);
323
630
  return;
324
631
  }
325
632
 
@@ -328,11 +635,11 @@ function printSuccess(outputMode, payload) {
328
635
  }
329
636
 
330
637
  if (payload.result) {
331
- process.stdout.write(`${tone('Result:', 'section')}\n${formatResultText(payload.result)}\n`);
638
+ printTextBlock('Result', formatResultText(payload.result));
332
639
  }
333
640
 
334
641
  if (payload.data && !payload.result) {
335
- process.stdout.write(`${tone('Details:', 'section')}\n${JSON.stringify(payload.data, null, 2)}\n`);
642
+ printTextBlock('Details', JSON.stringify(payload.data, null, 2));
336
643
  }
337
644
  }
338
645
 
@@ -351,11 +658,16 @@ function printError(outputMode, error) {
351
658
 
352
659
  process.stderr.write(`${badge('err')} ${error.message}\n`);
353
660
  if (error instanceof UsageError) {
354
- 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`);
355
663
  }
356
664
  }
357
665
 
358
666
  function buildOutputMode(options) {
667
+ if (asBoolean(readOption(options, 'ai', false), false)) {
668
+ return 'json';
669
+ }
670
+
359
671
  const outputOption = readOption(options, 'output');
360
672
  if (outputOption === 'json') {
361
673
  return 'json';
@@ -422,43 +734,49 @@ async function callTool(toolName, args, outputMode, commandLabel) {
422
734
  function printUsage() {
423
735
  const usage = [
424
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'),
425
739
  '',
426
- tone('Global install:', 'section'),
427
- ` npm i -g ${pkg.name}`,
740
+ tone('Install', 'section'),
741
+ formatRow('global', `npm i -g ${pkg.name}`, 18),
742
+ formatRow('local', 'npm install', 18),
428
743
  '',
429
- tone('Usage:', 'section'),
430
- ` ${cliCommand('<command> [subcommand] [options]')}`,
744
+ tone('Usage', 'section'),
745
+ ` ${tone(cliCommand('<command> [subcommand] [options]'), 'accent')}`,
431
746
  '',
432
- tone('Global options:', 'section'),
433
- ' --json Output machine-readable JSON',
434
- ' --output text|json Explicit output mode',
435
- ' --plain Disable rich colors and animations',
436
- ' --no-color Disable colors explicitly',
437
- ' --no-animate Disable spinner animations',
438
- ' --help Show help',
439
- ' --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'),
440
757
  '',
441
- tone('Core commands:', 'section'),
442
- ' commands Show all command groups and tools',
443
- ' tools list',
444
- ' tools schema <tool-name>',
445
- ' call <tool-name> [--args-json "{...}"] [--arg key=value]',
446
- ' auth status|url|login|logout|server',
447
- ' auth login [--client-id <id>] [--client-secret <secret>] [--prompt-credentials true|false]',
448
- ' auth server [--status] [--start]',
449
- ' update [--run] [--to latest|x.y.z]',
450
- ' 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)'),
451
768
  '',
452
- tone('Human-friendly command groups:', 'section'),
453
- ' email list|search|read|send|mark-read',
454
- ' calendar list|create|decline|cancel|delete',
455
- ' folder list|create|move',
456
- ' 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'),
457
774
  '',
458
- tone('Examples:', 'section'),
775
+ tone('Examples', 'section'),
459
776
  ` ${cliCommand('auth login --open --start-server --wait --timeout 180')}`,
460
- ` ${cliCommand('email list --folder inbox --count 15')}`,
461
- ` ${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")}`,
462
780
  ` ${cliCommand('tools schema send-email')}`,
463
781
  ` ${cliCommand('update --run')}`
464
782
  ];
@@ -553,9 +871,10 @@ function buildCommandCatalog() {
553
871
  calendar: ['list', 'create', 'decline', 'cancel', 'delete'],
554
872
  folder: ['list', 'create', 'move'],
555
873
  rule: ['list', 'create', 'sequence'],
874
+ agents: ['guide'],
556
875
  tools: ['list', 'schema'],
557
876
  generic: ['call'],
558
- system: ['doctor', 'update', 'version', 'help']
877
+ system: ['doctor', 'update', 'version', 'help', 'mcp-server']
559
878
  },
560
879
  tools: toolCatalog
561
880
  };
@@ -570,18 +889,21 @@ function printCommandCatalog(outputMode) {
570
889
  data: catalog,
571
890
  message: outputMode === 'text'
572
891
  ? [
573
- `Command groups:`,
574
- `- auth: ${catalog.commandGroups.auth.join(', ')}`,
575
- `- email: ${catalog.commandGroups.email.join(', ')}`,
576
- `- calendar: ${catalog.commandGroups.calendar.join(', ')}`,
577
- `- folder: ${catalog.commandGroups.folder.join(', ')}`,
578
- `- rule: ${catalog.commandGroups.rule.join(', ')}`,
579
- `- tools: ${catalog.commandGroups.tools.join(', ')}`,
580
- `- generic: ${catalog.commandGroups.generic.join(', ')}`,
581
- `- 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),
582
903
  '',
583
- `Available MCP tools (${catalog.tools.length}):`,
584
- ...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))
585
907
  ].join('\n')
586
908
  : undefined
587
909
  });
@@ -1290,6 +1612,56 @@ async function handleRuleCommand(action, options, outputMode) {
1290
1612
  }
1291
1613
  }
1292
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
+
1293
1665
  async function handleDoctorCommand(outputMode) {
1294
1666
  const tokenPath = config.AUTH_CONFIG.tokenStorePath;
1295
1667
  const tokenExists = fs.existsSync(tokenPath);
@@ -1445,6 +1817,11 @@ async function run() {
1445
1817
  await handleGenericCall(positional.slice(1), options, outputMode);
1446
1818
  return;
1447
1819
 
1820
+ case 'agents':
1821
+ case 'agent':
1822
+ await handleAgentsCommand(action, outputMode);
1823
+ return;
1824
+
1448
1825
  case 'auth':
1449
1826
  await handleAuthCommand(action, options, outputMode);
1450
1827
  return;
@@ -1473,6 +1850,10 @@ async function run() {
1473
1850
  await handleUpdateCommand(options, outputMode);
1474
1851
  return;
1475
1852
 
1853
+ case 'mcp-server':
1854
+ startMcpServerProcess();
1855
+ return;
1856
+
1476
1857
  default:
1477
1858
  throw new UsageError(`Unknown command: ${command}. Run '${cliCommand('help')}' for usage.`);
1478
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.1",
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",
@@ -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
  }