outlook-cli 1.2.1 → 1.2.3

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.js CHANGED
@@ -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];
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;
245
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,51 @@ 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|attachments|attachment|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('email attachments --id <message-id>')}`,
779
+ ` ${cliCommand('email attachment --id <message-id> --attachment-id <attachment-id> --save-path ./downloads/')}`,
780
+ ` ${cliCommand('agents guide --json')}`,
781
+ ` ${cliCommand("call list-emails --args-json '{\"folder\":\"inbox\",\"count\":5}' --ai")}`,
462
782
  ` ${cliCommand('tools schema send-email')}`,
463
783
  ` ${cliCommand('update --run')}`
464
784
  ];
@@ -549,13 +869,14 @@ function buildCommandCatalog() {
549
869
  return {
550
870
  commandGroups: {
551
871
  auth: ['status', 'url', 'login', 'logout', 'server'],
552
- email: ['list', 'search', 'read', 'send', 'mark-read'],
872
+ email: ['list', 'search', 'read', 'attachments', 'attachment', 'send', 'mark-read'],
553
873
  calendar: ['list', 'create', 'decline', 'cancel', 'delete'],
554
874
  folder: ['list', 'create', 'move'],
555
875
  rule: ['list', 'create', 'sequence'],
876
+ agents: ['guide'],
556
877
  tools: ['list', 'schema'],
557
878
  generic: ['call'],
558
- system: ['doctor', 'update', 'version', 'help']
879
+ system: ['doctor', 'update', 'version', 'help', 'mcp-server']
559
880
  },
560
881
  tools: toolCatalog
561
882
  };
@@ -570,18 +891,21 @@ function printCommandCatalog(outputMode) {
570
891
  data: catalog,
571
892
  message: outputMode === 'text'
572
893
  ? [
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(', ')}`,
894
+ tone('Command groups', 'section'),
895
+ tone(separator(48), 'border'),
896
+ formatRow('auth', catalog.commandGroups.auth.join(', '), 14),
897
+ formatRow('email', catalog.commandGroups.email.join(', '), 14),
898
+ formatRow('calendar', catalog.commandGroups.calendar.join(', '), 14),
899
+ formatRow('folder', catalog.commandGroups.folder.join(', '), 14),
900
+ formatRow('rule', catalog.commandGroups.rule.join(', '), 14),
901
+ formatRow('agents', catalog.commandGroups.agents.join(', '), 14),
902
+ formatRow('tools', catalog.commandGroups.tools.join(', '), 14),
903
+ formatRow('generic', catalog.commandGroups.generic.join(', '), 14),
904
+ formatRow('system', catalog.commandGroups.system.join(', '), 14),
582
905
  '',
583
- `Available MCP tools (${catalog.tools.length}):`,
584
- ...catalog.tools.map((tool) => `- ${tool.name}`)
906
+ tone(`Available MCP tools (${catalog.tools.length})`, 'section'),
907
+ tone(separator(48), 'border'),
908
+ ...catalog.tools.map((tool) => formatRow(tool.name, tool.description || 'No description', 28))
585
909
  ].join('\n')
586
910
  : undefined
587
911
  });
@@ -1079,6 +1403,36 @@ async function handleEmailCommand(action, options, outputMode) {
1079
1403
  return;
1080
1404
  }
1081
1405
 
1406
+ case 'attachments': {
1407
+ await callTool(
1408
+ 'list-attachments',
1409
+ {
1410
+ messageId: requireOption(options, 'id', `Usage: ${cliCommand('email attachments --id <email-id> [--count <number>]')}`),
1411
+ count: asNumber(readOption(options, 'count', 25), 25, 'count')
1412
+ },
1413
+ outputMode,
1414
+ 'email attachments'
1415
+ );
1416
+ return;
1417
+ }
1418
+
1419
+ case 'attachment': {
1420
+ await callTool(
1421
+ 'get-attachment',
1422
+ {
1423
+ messageId: requireOption(options, 'id', `Usage: ${cliCommand('email attachment --id <email-id> --attachment-id <attachment-id> [--save-path <path>]')}`),
1424
+ attachmentId: requireOption(options, 'attachmentId', `Usage: ${cliCommand('email attachment --id <email-id> --attachment-id <attachment-id> [--save-path <path>]')}`),
1425
+ savePath: readOption(options, 'savePath', readOption(options, 'out')),
1426
+ includeContent: asBoolean(readOption(options, 'includeContent', false), false),
1427
+ expandItem: asBoolean(readOption(options, 'expandItem', false), false),
1428
+ overwrite: asBoolean(readOption(options, 'overwrite', false), false)
1429
+ },
1430
+ outputMode,
1431
+ 'email attachment'
1432
+ );
1433
+ return;
1434
+ }
1435
+
1082
1436
  case 'send': {
1083
1437
  await callTool(
1084
1438
  'send-email',
@@ -1111,7 +1465,7 @@ async function handleEmailCommand(action, options, outputMode) {
1111
1465
  }
1112
1466
 
1113
1467
  default:
1114
- throw new UsageError('Unknown email command. Use: email list|search|read|send|mark-read');
1468
+ throw new UsageError('Unknown email command. Use: email list|search|read|attachments|attachment|send|mark-read');
1115
1469
  }
1116
1470
  }
1117
1471
 
@@ -1290,6 +1644,56 @@ async function handleRuleCommand(action, options, outputMode) {
1290
1644
  }
1291
1645
  }
1292
1646
 
1647
+ function startMcpServerProcess() {
1648
+ // eslint-disable-next-line global-require
1649
+ require('./index');
1650
+ }
1651
+
1652
+ async function handleAgentsCommand(action, outputMode) {
1653
+ const subcommand = action || 'guide';
1654
+ if (subcommand !== 'guide') {
1655
+ throw new UsageError('Unknown agents command. Use: agents guide');
1656
+ }
1657
+
1658
+ const tools = listTools();
1659
+ const guide = {
1660
+ totalTools: tools.length,
1661
+ toolNames: tools.map((tool) => tool.name),
1662
+ mcpServerCommand: `${CLI_BIN_NAME} mcp-server`,
1663
+ recommendedFlow: [
1664
+ `${CLI_BIN_NAME} auth status --json`,
1665
+ `${CLI_BIN_NAME} tools list --json`,
1666
+ `${CLI_BIN_NAME} tools schema <tool-name> --json`,
1667
+ `${CLI_BIN_NAME} call <tool-name> --args-json '{"...":"..."}' --json`
1668
+ ],
1669
+ safetyNotes: [
1670
+ 'Prefer --json or --ai for agent workflows.',
1671
+ 'Use tools schema before call for strict argument validation.',
1672
+ 'Use auth status before calling Microsoft Graph tools.'
1673
+ ]
1674
+ };
1675
+
1676
+ printSuccess(outputMode, {
1677
+ ok: true,
1678
+ command: 'agents guide',
1679
+ data: guide,
1680
+ message: outputMode === 'text'
1681
+ ? [
1682
+ tone('Agent guide', 'section'),
1683
+ tone(separator(48), 'border'),
1684
+ formatRow('mcp server', guide.mcpServerCommand, 18),
1685
+ formatRow('available tools', String(guide.totalTools), 18),
1686
+ '',
1687
+ tone('Recommended workflow', 'section'),
1688
+ ...guide.recommendedFlow.map((line) => ` ${tone(line, 'accent')}`),
1689
+ '',
1690
+ tone('Best practices', 'section'),
1691
+ ...guide.safetyNotes.map((note) => formatRow('note', note, 18))
1692
+ ].join('\n')
1693
+ : undefined
1694
+ });
1695
+ }
1696
+
1293
1697
  async function handleDoctorCommand(outputMode) {
1294
1698
  const tokenPath = config.AUTH_CONFIG.tokenStorePath;
1295
1699
  const tokenExists = fs.existsSync(tokenPath);
@@ -1445,6 +1849,11 @@ async function run() {
1445
1849
  await handleGenericCall(positional.slice(1), options, outputMode);
1446
1850
  return;
1447
1851
 
1852
+ case 'agents':
1853
+ case 'agent':
1854
+ await handleAgentsCommand(action, outputMode);
1855
+ return;
1856
+
1448
1857
  case 'auth':
1449
1858
  await handleAuthCommand(action, options, outputMode);
1450
1859
  return;
@@ -1473,6 +1882,10 @@ async function run() {
1473
1882
  await handleUpdateCommand(options, outputMode);
1474
1883
  return;
1475
1884
 
1885
+ case 'mcp-server':
1886
+ startMcpServerProcess();
1887
+ return;
1888
+
1476
1889
  default:
1477
1890
  throw new UsageError(`Unknown command: ${command}. Run '${cliCommand('help')}' for usage.`);
1478
1891
  }