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