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.md +16 -0
- package/README.md +110 -5
- package/cli.js +483 -70
- package/docs/REFERENCE.md +40 -2
- package/email/attachments.js +359 -0
- package/email/index.js +58 -1
- package/email/search.js +18 -12
- package/package.json +1 -1
- package/utils/graph-api.js +132 -44
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
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
638
|
+
printTextBlock('Result', formatResultText(payload.result));
|
|
332
639
|
}
|
|
333
640
|
|
|
334
641
|
if (payload.data && !payload.result) {
|
|
335
|
-
|
|
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(
|
|
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('
|
|
427
|
-
`
|
|
740
|
+
tone('Install', 'section'),
|
|
741
|
+
formatRow('global', `npm i -g ${pkg.name}`, 18),
|
|
742
|
+
formatRow('local', 'npm install', 18),
|
|
428
743
|
'',
|
|
429
|
-
tone('Usage
|
|
430
|
-
` ${cliCommand('<command> [subcommand] [options]')}`,
|
|
744
|
+
tone('Usage', 'section'),
|
|
745
|
+
` ${tone(cliCommand('<command> [subcommand] [options]'), 'accent')}`,
|
|
431
746
|
'',
|
|
432
|
-
tone('Global options
|
|
433
|
-
'
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
'
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'
|
|
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
|
|
442
|
-
'
|
|
443
|
-
'
|
|
444
|
-
'
|
|
445
|
-
'
|
|
446
|
-
'
|
|
447
|
-
'
|
|
448
|
-
'
|
|
449
|
-
'
|
|
450
|
-
'
|
|
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('
|
|
453
|
-
'
|
|
454
|
-
'
|
|
455
|
-
'
|
|
456
|
-
'
|
|
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
|
|
775
|
+
tone('Examples', 'section'),
|
|
459
776
|
` ${cliCommand('auth login --open --start-server --wait --timeout 180')}`,
|
|
460
|
-
` ${cliCommand('
|
|
461
|
-
` ${cliCommand('
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
}
|