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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm version](https://img.shields.io/npm/v/msteams-mcp.svg)](https://www.npmjs.com/package/msteams-mcp)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/msteams-mcp.svg)](https://www.npmjs.com/package/msteams-mcp)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org/)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](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 18+
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. Run with no arguments to list all tools and shortcuts.
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
- # List available tools and shortcuts
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 teams_search_email --query "from:sarah@company.com"
296
+ msteams email "from:sarah@company.com"
292
297
 
293
- # Send a message (default: your own notes/self-chat)
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
- # Any tool by name (the teams_ prefix is optional)
304
- msteams teams_search_emoji --query "heart"
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> [primaryArg] [--key value ...]`. Any unrecognised command is treated as a tool name (`teams_` is added automatically). Common flags like `--to`, `--from`, `--size`, `--query`, `--force` map to the matching tool parameters; run `msteams` with no arguments to see the full list.
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 tools
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: find any origin containing 'teams.microsoft'
181
- return state.origins.find(o => o.origin.includes('teams.microsoft') || o.origin.includes('teams.cloud')) ?? null;
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
  }
@@ -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 {};
@@ -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
- // If on Teams domain, check for authenticated content
194
- if (currentUrl.includes('teams.microsoft.com')) {
195
- const hasContent = await hasAuthenticatedContent(page);
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: hasContent,
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 TEAMS_URL_PATTERNS.some(pattern => url.includes(pattern));
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;
@@ -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[];