msteams-mcp 0.29.1 → 0.30.0
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/README.md +23 -17
- package/dist/auth/session-store.js +4 -9
- package/dist/browser/auth.d.ts +14 -0
- package/dist/browser/auth.js +12 -40
- package/dist/browser/auth.test.d.ts +1 -0
- package/dist/browser/auth.test.js +45 -0
- package/dist/browser/context.test.d.ts +8 -0
- package/dist/browser/context.test.js +55 -0
- package/dist/cli/help.d.ts +21 -0
- package/dist/cli/help.js +111 -0
- package/dist/cli/render.d.ts +21 -0
- package/dist/cli/render.js +155 -0
- package/dist/cli/resolve.d.ts +50 -0
- package/dist/cli/resolve.js +197 -0
- package/dist/cli/resolve.test.d.ts +8 -0
- package/dist/cli/resolve.test.js +118 -0
- package/dist/cli.d.ts +12 -12
- package/dist/cli.js +93 -450
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +15 -0
- package/dist/index.js +0 -0
- package/dist/tools/auth-tools.js +30 -1
- package/dist/tools/auth-tools.test.d.ts +1 -0
- package/dist/tools/auth-tools.test.js +67 -0
- package/dist/tools/message-tools.js +3 -3
- package/dist/tools/registry.d.ts +8 -0
- package/dist/tools/registry.js +12 -0
- package/dist/types/server.d.ts +27 -0
- package/dist/types/server.js +7 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -2
- package/dist/utils/logger.test.js +28 -53
- package/dist/utils/parsers-markdown.d.ts +19 -12
- package/dist/utils/parsers-markdown.js +153 -95
- package/dist/utils/parsers.test.js +56 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/msteams-mcp)
|
|
5
5
|
[](https://www.npmjs.com/package/msteams-mcp)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://nodejs.org/)
|
|
8
8
|
|
|
9
9
|
Give AI access to Microsoft Teams easily with your standard Teams login.
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ Search messages and email, catch up on unread chats, read threads, send replies,
|
|
|
27
27
|
|
|
28
28
|
### Prerequisites
|
|
29
29
|
|
|
30
|
-
- Node.js
|
|
30
|
+
- Node.js 20+
|
|
31
31
|
- A Microsoft account with Teams access
|
|
32
32
|
- Google Chrome, Microsoft Edge, or Chromium browser installed
|
|
33
33
|
|
|
@@ -102,7 +102,7 @@ This installs two binaries: `msteams-mcp` (the MCP server) and `msteams` (the CL
|
|
|
102
102
|
npx -y msteams-mcp msteams status
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
See [CLI Usage](#cli-usage) for commands.
|
|
105
|
+
See [CLI Usage](#cli-usage) for commands, or run `msteams` for the command list.
|
|
106
106
|
|
|
107
107
|
## Available Tools
|
|
108
108
|
|
|
@@ -268,13 +268,18 @@ The server also exposes passive resources for context discovery:
|
|
|
268
268
|
|
|
269
269
|
## CLI Usage
|
|
270
270
|
|
|
271
|
-
`msteams` exposes every tool the MCP server does - full parity, same authentication, same session files.
|
|
271
|
+
`msteams` exposes every tool the MCP server does - full parity, same authentication, same session files. Commands, flags and help are generated from the tool definitions, so the CLI never lags behind the server.
|
|
272
272
|
|
|
273
273
|
If you installed globally (`npm install -g msteams-mcp`), invoke it directly:
|
|
274
274
|
|
|
275
275
|
```bash
|
|
276
|
-
#
|
|
276
|
+
# The command list, one line each
|
|
277
277
|
msteams
|
|
278
|
+
msteams --version
|
|
279
|
+
|
|
280
|
+
# What one command does, and all of its flags
|
|
281
|
+
msteams help send_message
|
|
282
|
+
msteams search --help
|
|
278
283
|
|
|
279
284
|
# Check authentication status
|
|
280
285
|
msteams status
|
|
@@ -288,9 +293,9 @@ msteams search "meeting notes"
|
|
|
288
293
|
msteams search "project" --from 0 --size 50
|
|
289
294
|
|
|
290
295
|
# Search emails
|
|
291
|
-
msteams
|
|
296
|
+
msteams email "from:sarah@company.com"
|
|
292
297
|
|
|
293
|
-
# Send a message (default: your own
|
|
298
|
+
# Send a message (default: your own self-chat, 48:notes)
|
|
294
299
|
msteams send "Hello from Teams MCP!"
|
|
295
300
|
msteams send "Message" --to "conversation-id"
|
|
296
301
|
|
|
@@ -300,29 +305,30 @@ msteams favorites
|
|
|
300
305
|
msteams activity
|
|
301
306
|
msteams unread
|
|
302
307
|
|
|
303
|
-
#
|
|
304
|
-
msteams
|
|
305
|
-
msteams find_channel --query "support"
|
|
306
|
-
|
|
307
|
-
# Machine-readable output
|
|
308
|
-
msteams search "query" --json
|
|
308
|
+
# Machine-readable output, on stdout, for scripts
|
|
309
|
+
msteams search "query" --json | jq '.results[0].conversationId'
|
|
309
310
|
```
|
|
310
311
|
|
|
311
|
-
**Command form:** `msteams <command> [
|
|
312
|
+
**Command form:** `msteams <command> [value] [--flag value]`.
|
|
313
|
+
|
|
314
|
+
- **Commands** are the tool names without the `teams_`/`get_` prefix (`search`, `send_message`, `thread`, `meetings`). Any unambiguous abbreviation works, so `send` reaches `send_message`; the full tool name always works too.
|
|
315
|
+
- **Flags** are the tool's own parameters, in camelCase (`--conversationId`, `--threadRootId`, `--fromUrl`). A few short aliases exist where they read better: `--to`, `--msg`, `--reply`, `--user`, `--force`. Booleans work bare (`--markRead`) or with a value (`--markRead false`). Unknown flags are rejected with the list of valid ones.
|
|
316
|
+
- **The first bare value** goes to the command's main parameter: `msteams search "query"` is `--query "query"`.
|
|
317
|
+
- **Output** goes to stdout, diagnostics to stderr, so pipes and redirects behave. Colour and emoji appear only on a terminal, and `NO_COLOR` turns them off.
|
|
318
|
+
- **Exit codes:** `0` success, `1` the tool failed, `2` the command was malformed.
|
|
319
|
+
- **`--json`** prints the tool's JSON payload, ready for `jq`.
|
|
312
320
|
|
|
313
321
|
### From a repo clone
|
|
314
322
|
|
|
315
323
|
If you're working from source, the same CLI is wired to `npm run cli` (runs via `tsx`, no build needed):
|
|
316
324
|
|
|
317
325
|
```bash
|
|
318
|
-
npm run cli # list
|
|
326
|
+
npm run cli # command list
|
|
319
327
|
npm run cli -- search "your query"
|
|
320
328
|
npm run cli -- status
|
|
321
329
|
npm run cli -- send "Hi" --to "conversation-id"
|
|
322
330
|
```
|
|
323
331
|
|
|
324
|
-
|
|
325
|
-
|
|
326
332
|
## Limitations
|
|
327
333
|
|
|
328
334
|
- **Login required** - Run `teams_login` to authenticate (opens browser)
|
|
@@ -13,7 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import { encrypt, decrypt } from './crypto.js';
|
|
16
|
-
import { SESSION_EXPIRY_HOURS } from '../constants.js';
|
|
16
|
+
import { SESSION_EXPIRY_HOURS, TEAMS_HOSTS } from '../constants.js';
|
|
17
17
|
import * as log from '../utils/logger.js';
|
|
18
18
|
/**
|
|
19
19
|
* User-specific config directory for teams-mcp-server.
|
|
@@ -131,12 +131,7 @@ export function clearTokenCache() {
|
|
|
131
131
|
* Known Teams origins (commercial and government clouds).
|
|
132
132
|
* Used to find the correct origin in session state.
|
|
133
133
|
*/
|
|
134
|
-
const TEAMS_ORIGINS =
|
|
135
|
-
'https://teams.cloud.microsoft', // New Teams URL (often holds SubstrateSearch tokens)
|
|
136
|
-
'https://teams.microsoft.com', // Commercial legacy
|
|
137
|
-
'https://teams.microsoft.us', // GCC-High
|
|
138
|
-
'https://dod.teams.microsoft.us', // DoD
|
|
139
|
-
];
|
|
134
|
+
const TEAMS_ORIGINS = TEAMS_HOSTS.map(host => `https://${host}`);
|
|
140
135
|
/**
|
|
141
136
|
* Returns true if localStorage has a SubstrateSearch MSAL token entry.
|
|
142
137
|
*/
|
|
@@ -177,6 +172,6 @@ export function getTeamsOrigin(state) {
|
|
|
177
172
|
if (origin)
|
|
178
173
|
return origin;
|
|
179
174
|
}
|
|
180
|
-
// Fallback:
|
|
181
|
-
return state.origins.find(o =>
|
|
175
|
+
// Fallback: any origin on a known Teams host
|
|
176
|
+
return state.origins.find(o => TEAMS_HOSTS.some(host => o.origin.includes(host))) ?? null;
|
|
182
177
|
}
|
package/dist/browser/auth.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
* Manages login detection and manual authentication flows.
|
|
4
4
|
*/
|
|
5
5
|
import type { Page, BrowserContext } from 'playwright';
|
|
6
|
+
interface AuthStatus {
|
|
7
|
+
isAuthenticated: boolean;
|
|
8
|
+
isOnLoginPage: boolean;
|
|
9
|
+
currentUrl: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Gets the current authentication status.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAuthStatus(page: Page): Promise<AuthStatus>;
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a URL is a Teams domain.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isTeamsUrl(url: string): boolean;
|
|
6
19
|
/**
|
|
7
20
|
* Performs a full authentication flow:
|
|
8
21
|
* 1. Navigate to Teams
|
|
@@ -16,3 +29,4 @@ import type { Page, BrowserContext } from 'playwright';
|
|
|
16
29
|
* @param headless - If true, throw immediately if user interaction is required (default: false)
|
|
17
30
|
*/
|
|
18
31
|
export declare function ensureAuthenticated(page: Page, context: BrowserContext, onProgress?: (message: string) => void, showOverlay?: boolean, headless?: boolean): Promise<void>;
|
|
32
|
+
export {};
|
package/dist/browser/auth.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages login detection and manual authentication flows.
|
|
4
4
|
*/
|
|
5
5
|
import { saveSessionState } from './context.js';
|
|
6
|
-
import { OVERLAY_STEP_PAUSE_MS, OVERLAY_COMPLETE_PAUSE_MS, } from '../constants.js';
|
|
6
|
+
import { OVERLAY_STEP_PAUSE_MS, OVERLAY_COMPLETE_PAUSE_MS, TEAMS_HOSTS, } from '../constants.js';
|
|
7
7
|
import { extractSubstrateToken } from '../auth/token-extractor.js';
|
|
8
8
|
import * as logger from '../utils/logger.js';
|
|
9
9
|
/**
|
|
@@ -145,41 +145,16 @@ const LOGIN_URL_PATTERNS = [
|
|
|
145
145
|
'login.live.com',
|
|
146
146
|
'login.microsoft.com',
|
|
147
147
|
];
|
|
148
|
-
// Selectors that indicate successful authentication
|
|
149
|
-
const AUTH_SUCCESS_SELECTORS = [
|
|
150
|
-
'[data-tid="app-bar"]',
|
|
151
|
-
'[data-tid="search-box"]',
|
|
152
|
-
'input[placeholder*="Search"]',
|
|
153
|
-
'[data-tid="chat-list"]',
|
|
154
|
-
'[data-tid="team-list"]',
|
|
155
|
-
];
|
|
156
148
|
/**
|
|
157
149
|
* Checks if the current page URL indicates a login flow.
|
|
158
150
|
*/
|
|
159
151
|
function isLoginUrl(url) {
|
|
160
152
|
return LOGIN_URL_PATTERNS.some(pattern => url.includes(pattern));
|
|
161
153
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Checks if the page shows authenticated Teams content.
|
|
164
|
-
*/
|
|
165
|
-
async function hasAuthenticatedContent(page) {
|
|
166
|
-
for (const selector of AUTH_SUCCESS_SELECTORS) {
|
|
167
|
-
try {
|
|
168
|
-
const count = await page.locator(selector).count();
|
|
169
|
-
if (count > 0) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
// Selector not found, continue checking others
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
154
|
/**
|
|
180
155
|
* Gets the current authentication status.
|
|
181
156
|
*/
|
|
182
|
-
async function getAuthStatus(page) {
|
|
157
|
+
export async function getAuthStatus(page) {
|
|
183
158
|
const currentUrl = page.url();
|
|
184
159
|
const onLoginPage = isLoginUrl(currentUrl);
|
|
185
160
|
// If on login page, definitely not authenticated
|
|
@@ -190,11 +165,15 @@ async function getAuthStatus(page) {
|
|
|
190
165
|
currentUrl,
|
|
191
166
|
};
|
|
192
167
|
}
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
168
|
+
// On a Teams domain, a live MSAL token is the proof of sign-in: it is what
|
|
169
|
+
// every API call needs and what the saved session is for. Teams' own DOM
|
|
170
|
+
// markers are not a usable signal — new Teams renders none of the `data-tid`
|
|
171
|
+
// elements earlier versions exposed, so waiting for them left a completed
|
|
172
|
+
// login undetected and the browser window open until the manual-login
|
|
173
|
+
// timeout.
|
|
174
|
+
if (isTeamsUrl(currentUrl)) {
|
|
196
175
|
return {
|
|
197
|
-
isAuthenticated:
|
|
176
|
+
isAuthenticated: await checkBrowserTokenExpiry(page) > 0,
|
|
198
177
|
isOnLoginPage: false,
|
|
199
178
|
currentUrl,
|
|
200
179
|
};
|
|
@@ -208,18 +187,11 @@ async function getAuthStatus(page) {
|
|
|
208
187
|
}
|
|
209
188
|
/** Timeout for detecting login redirect (ms). */
|
|
210
189
|
const LOGIN_REDIRECT_TIMEOUT_MS = 5000;
|
|
211
|
-
/** URL patterns that indicate we're on a Teams page (not redirected elsewhere). */
|
|
212
|
-
const TEAMS_URL_PATTERNS = [
|
|
213
|
-
'teams.microsoft.com',
|
|
214
|
-
'teams.microsoft.us', // GCC-High
|
|
215
|
-
'dod.teams.microsoft.us', // DoD
|
|
216
|
-
'teams.cloud.microsoft', // New Teams URL
|
|
217
|
-
];
|
|
218
190
|
/**
|
|
219
191
|
* Checks if a URL is a Teams domain.
|
|
220
192
|
*/
|
|
221
|
-
function isTeamsUrl(url) {
|
|
222
|
-
return
|
|
193
|
+
export function isTeamsUrl(url) {
|
|
194
|
+
return TEAMS_HOSTS.some(host => url.includes(host));
|
|
223
195
|
}
|
|
224
196
|
/**
|
|
225
197
|
* Navigates to Teams and checks authentication status.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
vi.mock('./context.js', () => ({ saveSessionState: vi.fn() }));
|
|
3
|
+
vi.mock('../auth/token-extractor.js', () => ({ extractSubstrateToken: vi.fn() }));
|
|
4
|
+
import { getAuthStatus, isTeamsUrl } from './auth.js';
|
|
5
|
+
/** A page on `url` with no Teams DOM markers and `tokenMinutes` left on MSAL. */
|
|
6
|
+
function fakePage(url, tokenMinutes) {
|
|
7
|
+
return {
|
|
8
|
+
url: () => url,
|
|
9
|
+
locator: () => ({ count: async () => 0 }),
|
|
10
|
+
evaluate: async () => tokenMinutes,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe('isTeamsUrl', () => {
|
|
14
|
+
it('covers every host Microsoft redirects Teams users to', () => {
|
|
15
|
+
// New Teams settles on the bare host, with no path.
|
|
16
|
+
for (const url of [
|
|
17
|
+
'https://teams.microsoft.com/',
|
|
18
|
+
'https://teams.cloud.microsoft/',
|
|
19
|
+
'https://teams.chat.microsoft.com/',
|
|
20
|
+
'https://teams.microsoft.us/',
|
|
21
|
+
'https://dod.teams.microsoft.us/',
|
|
22
|
+
]) {
|
|
23
|
+
expect(isTeamsUrl(url), url).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
expect(isTeamsUrl('https://outlook.office.com/')).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('matches on the host, whatever path the app has routed to', () => {
|
|
28
|
+
expect(isTeamsUrl('https://teams.microsoft.com/v2/#/conversations/19:abc@thread.tacv2')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('getAuthStatus', () => {
|
|
32
|
+
it('detects a completed login from the MSAL token alone', async () => {
|
|
33
|
+
const status = await getAuthStatus(fakePage('https://teams.cloud.microsoft/', 55));
|
|
34
|
+
expect(status.isAuthenticated).toBe(true);
|
|
35
|
+
expect(status.isOnLoginPage).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it('stays unauthenticated on a Teams host with no token', async () => {
|
|
38
|
+
const status = await getAuthStatus(fakePage('https://teams.cloud.microsoft/', -1));
|
|
39
|
+
expect(status.isAuthenticated).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('reports the login page', async () => {
|
|
42
|
+
const status = await getAuthStatus(fakePage('https://login.microsoftonline.com/common/oauth2', 99));
|
|
43
|
+
expect(status).toMatchObject({ isAuthenticated: false, isOnLoginPage: true });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for browser channel selection.
|
|
3
|
+
*
|
|
4
|
+
* The server launches into its own profile dir, so the user's default browser
|
|
5
|
+
* is irrelevant - what matters is that every platform has an ordered list of
|
|
6
|
+
* channels to try and ends with a fallback that needs no system browser.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for browser channel selection.
|
|
3
|
+
*
|
|
4
|
+
* The server launches into its own profile dir, so the user's default browser
|
|
5
|
+
* is irrelevant - what matters is that every platform has an ordered list of
|
|
6
|
+
* channels to try and ends with a fallback that needs no system browser.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
9
|
+
import { browserChannels } from './context.js';
|
|
10
|
+
/** Runs fn with process.platform stubbed, then restores the real value. */
|
|
11
|
+
function withPlatform(platform, fn) {
|
|
12
|
+
const original = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
13
|
+
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
|
|
14
|
+
try {
|
|
15
|
+
return fn();
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
Object.defineProperty(process, 'platform', original);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
describe('browserChannels', () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.unstubAllEnvs();
|
|
24
|
+
});
|
|
25
|
+
it('prefers Edge on Windows, Chrome elsewhere', () => {
|
|
26
|
+
vi.stubEnv('TEAMS_BROWSER_CHANNEL', '');
|
|
27
|
+
expect(withPlatform('win32', browserChannels)[0]).toBe('msedge');
|
|
28
|
+
expect(withPlatform('darwin', browserChannels)[0]).toBe('chrome');
|
|
29
|
+
expect(withPlatform('linux', browserChannels)[0]).toBe('chrome');
|
|
30
|
+
});
|
|
31
|
+
it('offers both system channels on every platform', () => {
|
|
32
|
+
vi.stubEnv('TEAMS_BROWSER_CHANNEL', '');
|
|
33
|
+
for (const platform of ['win32', 'darwin', 'linux']) {
|
|
34
|
+
const channels = withPlatform(platform, browserChannels);
|
|
35
|
+
expect(channels).toContain('chrome');
|
|
36
|
+
expect(channels).toContain('msedge');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
it('ends with bundled Chromium so a machine with no system browser can recover', () => {
|
|
40
|
+
vi.stubEnv('TEAMS_BROWSER_CHANNEL', '');
|
|
41
|
+
for (const platform of ['win32', 'darwin', 'linux']) {
|
|
42
|
+
expect(withPlatform(platform, browserChannels).at(-1)).toBeUndefined();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('honours TEAMS_BROWSER_CHANNEL and disables the fallback', () => {
|
|
46
|
+
vi.stubEnv('TEAMS_BROWSER_CHANNEL', 'msedge');
|
|
47
|
+
expect(withPlatform('darwin', browserChannels)).toEqual(['msedge']);
|
|
48
|
+
vi.stubEnv('TEAMS_BROWSER_CHANNEL', 'chrome');
|
|
49
|
+
expect(withPlatform('win32', browserChannels)).toEqual(['chrome']);
|
|
50
|
+
});
|
|
51
|
+
it('ignores an unrecognised TEAMS_BROWSER_CHANNEL', () => {
|
|
52
|
+
vi.stubEnv('TEAMS_BROWSER_CHANNEL', 'safari');
|
|
53
|
+
expect(withPlatform('darwin', browserChannels)).toEqual(['chrome', 'msedge', undefined]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help text for the `msteams` CLI, generated from the tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* The index stays to one screen: one line per command, first sentence only.
|
|
5
|
+
* Full parameter detail lives in `msteams help <command>`, and the complete
|
|
6
|
+
* schemas in `msteams help --json`.
|
|
7
|
+
*/
|
|
8
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
/**
|
|
10
|
+
* First sentence of a description. The tool descriptions are written for an
|
|
11
|
+
* LLM and run long, so only the opening sentence is useful in a command list.
|
|
12
|
+
* A period followed by a non-capital (as in "e.g.") does not end a sentence.
|
|
13
|
+
*/
|
|
14
|
+
export declare function firstSentence(text: string): string;
|
|
15
|
+
/** The one-screen command index. */
|
|
16
|
+
export declare function renderIndex(tools: Tool[], groups: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
tools: string[];
|
|
19
|
+
}>, version: string): string;
|
|
20
|
+
/** Detail for one command: what it does, how to call it, every flag. */
|
|
21
|
+
export declare function renderToolHelp(tools: Tool[], tool: Tool): string;
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help text for the `msteams` CLI, generated from the tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* The index stays to one screen: one line per command, first sentence only.
|
|
5
|
+
* Full parameter detail lives in `msteams help <command>`, and the complete
|
|
6
|
+
* schemas in `msteams help --json`.
|
|
7
|
+
*/
|
|
8
|
+
import { FLAG_ALIASES, positionalProp, requiredProps, schemaProps, shortName } from './resolve.js';
|
|
9
|
+
import { bold, dim, termWidth, wrap } from './render.js';
|
|
10
|
+
/**
|
|
11
|
+
* First sentence of a description. The tool descriptions are written for an
|
|
12
|
+
* LLM and run long, so only the opening sentence is useful in a command list.
|
|
13
|
+
* A period followed by a non-capital (as in "e.g.") does not end a sentence.
|
|
14
|
+
*/
|
|
15
|
+
export function firstSentence(text) {
|
|
16
|
+
const match = /^(.*?[.!?])(?=\s+[A-Z]|$)/s.exec(text.trim());
|
|
17
|
+
const sentence = (match?.[1] ?? text).replace(/\s+/g, ' ').trim();
|
|
18
|
+
return sentence.replace(/\.$/, '');
|
|
19
|
+
}
|
|
20
|
+
function flagLabel(prop) {
|
|
21
|
+
if (prop.enum)
|
|
22
|
+
return `<${prop.enum.join('|')}>`;
|
|
23
|
+
if (prop.type === 'boolean')
|
|
24
|
+
return '';
|
|
25
|
+
if (prop.type === 'array')
|
|
26
|
+
return '<a,b>';
|
|
27
|
+
return `<${prop.type ?? 'string'}>`;
|
|
28
|
+
}
|
|
29
|
+
/** Aliases pointing at a parameter, e.g. conversationId -> ['--to']. */
|
|
30
|
+
function aliasesFor(prop) {
|
|
31
|
+
return Object.entries(FLAG_ALIASES)
|
|
32
|
+
.filter(([, target]) => target === prop)
|
|
33
|
+
.map(([alias]) => `--${alias}`);
|
|
34
|
+
}
|
|
35
|
+
/** The one-screen command index. */
|
|
36
|
+
export function renderIndex(tools, groups, version) {
|
|
37
|
+
const names = tools.map(tool => tool.name);
|
|
38
|
+
const byName = new Map(tools.map(tool => [tool.name, tool]));
|
|
39
|
+
const labels = new Map(names.map(name => [name, shortName(names, name)]));
|
|
40
|
+
const column = Math.min(22, Math.max(...[...labels.values()].map(label => label.length)) + 2);
|
|
41
|
+
const lines = [
|
|
42
|
+
`${bold('msteams')} ${dim(version)} - Microsoft Teams from your terminal`,
|
|
43
|
+
'',
|
|
44
|
+
bold('Usage'),
|
|
45
|
+
' msteams <command> [value] [--flag value]',
|
|
46
|
+
` msteams help <command> ${dim('flags and detail for one command')}`,
|
|
47
|
+
` msteams <command> --json ${dim('machine-readable output')}`,
|
|
48
|
+
];
|
|
49
|
+
for (const group of groups) {
|
|
50
|
+
const members = group.tools.filter(name => byName.has(name));
|
|
51
|
+
if (members.length === 0)
|
|
52
|
+
continue;
|
|
53
|
+
lines.push('', bold(group.name));
|
|
54
|
+
for (const name of members) {
|
|
55
|
+
const label = labels.get(name) ?? name;
|
|
56
|
+
const summary = firstSentence(byName.get(name)?.description ?? '');
|
|
57
|
+
const room = termWidth() - column - 4;
|
|
58
|
+
lines.push(` ${label.padEnd(column)}${dim(summary.length > room ? `${summary.slice(0, room - 1)}…` : summary)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lines.push('', dim('Commands are tool names without the teams_/get_ prefix; any unambiguous'), dim("abbreviation works (send -> send_message). 'msteams login' first."));
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
/** Detail for one command: what it does, how to call it, every flag. */
|
|
65
|
+
export function renderToolHelp(tools, tool) {
|
|
66
|
+
const names = tools.map(t => t.name);
|
|
67
|
+
const label = shortName(names, tool.name);
|
|
68
|
+
const props = schemaProps(tool);
|
|
69
|
+
const required = new Set(requiredProps(tool));
|
|
70
|
+
const positional = positionalProp(tool);
|
|
71
|
+
const usage = [` msteams ${label}`];
|
|
72
|
+
if (positional)
|
|
73
|
+
usage[0] += ` <${positional}>`;
|
|
74
|
+
if (Object.keys(props).length > (positional ? 1 : 0))
|
|
75
|
+
usage[0] += ' [--flag value]';
|
|
76
|
+
const lines = [
|
|
77
|
+
`${bold(`msteams ${label}`)} ${dim(`(${tool.name})`)}`,
|
|
78
|
+
'',
|
|
79
|
+
...wrap(firstSentence(tool.description ?? ''), 0, 3).map(line => ` ${line}`),
|
|
80
|
+
'',
|
|
81
|
+
bold('Usage'),
|
|
82
|
+
...usage,
|
|
83
|
+
];
|
|
84
|
+
const entries = Object.entries(props);
|
|
85
|
+
if (entries.length > 0) {
|
|
86
|
+
lines.push('', bold('Flags'));
|
|
87
|
+
const column = Math.min(28, Math.max(...entries.map(([name, prop]) => `--${name} ${flagLabel(prop)}`.trim().length)) + 2);
|
|
88
|
+
for (const [name, prop] of entries) {
|
|
89
|
+
const flag = `--${name} ${flagLabel(prop)}`.trim();
|
|
90
|
+
const alias = aliasesFor(name);
|
|
91
|
+
const detail = [
|
|
92
|
+
required.has(name) ? 'Required.' : '',
|
|
93
|
+
alias.length ? `(or ${alias.join(', ')})` : '',
|
|
94
|
+
prop.description ?? '',
|
|
95
|
+
].filter(Boolean).join(' ');
|
|
96
|
+
const wrapped = wrap(detail, column + 2, 2);
|
|
97
|
+
// A flag too long for the column takes a line of its own.
|
|
98
|
+
if (flag.length >= column) {
|
|
99
|
+
lines.push(` ${bold(flag)}`);
|
|
100
|
+
for (const line of wrapped)
|
|
101
|
+
lines.push(` ${' '.repeat(column)}${line}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
lines.push(` ${bold(flag.padEnd(column))}${wrapped[0] ?? ''}`);
|
|
105
|
+
for (const line of wrapped.slice(1))
|
|
106
|
+
lines.push(` ${' '.repeat(column)}${line}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
lines.push('', dim('Add --json for the raw payload.'));
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output for the `msteams` CLI.
|
|
3
|
+
*
|
|
4
|
+
* One generic record renderer covers every tool response: anything with an
|
|
5
|
+
* array gets a numbered list, anything else gets labelled fields. Colour and
|
|
6
|
+
* emoji only appear on a TTY, so piped output stays clean.
|
|
7
|
+
*/
|
|
8
|
+
export declare const bold: (text: string) => string;
|
|
9
|
+
export declare const dim: (text: string) => string;
|
|
10
|
+
export declare const icon: (emoji: string) => string;
|
|
11
|
+
/** Terminal width, with a sane default when output is piped. */
|
|
12
|
+
export declare const termWidth: () => number;
|
|
13
|
+
export declare function oneLine(text: string, max?: number): string;
|
|
14
|
+
/** Renders any field value as a single line, or '' when there is nothing to show. */
|
|
15
|
+
export declare function summarise(value: unknown, max?: number, depth?: number): string;
|
|
16
|
+
/** Prints a successful tool response. */
|
|
17
|
+
export declare function printResponse(response: Record<string, unknown>): void;
|
|
18
|
+
/** Prints a failed tool response to stderr. */
|
|
19
|
+
export declare function printFailure(response: Record<string, unknown>): void;
|
|
20
|
+
/** Wraps text to the terminal width with a hanging indent, clipped to maxLines. */
|
|
21
|
+
export declare function wrap(text: string, indent: number, maxLines?: number): string[];
|