teams-api-mcp 0.1.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.
@@ -0,0 +1,842 @@
1
+ "use strict";
2
+ /**
3
+ * Unified action definitions for the Teams API.
4
+ *
5
+ * This is the single source of truth for all operations. CLI commands,
6
+ * MCP tools, and programmatic usage all derive from these definitions.
7
+ *
8
+ * Each action declares:
9
+ * - name, title, description — shared help text and documentation
10
+ * - parameters — typed parameter definitions with descriptions and defaults
11
+ * - execute — the implementation, calling TeamsClient methods
12
+ * - formatResult — human-readable output formatter (CLI without --json)
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.actions = void 0;
16
+ exports.formatOutput = formatOutput;
17
+ /** Format an action result in the specified output format. */
18
+ function formatOutput(action, result, format = "json") {
19
+ switch (format) {
20
+ case "json":
21
+ return JSON.stringify(result, null, 2);
22
+ case "text":
23
+ return action.formatResult(result);
24
+ case "md":
25
+ return action.formatMarkdown(result);
26
+ case "toon":
27
+ return action.formatToon(result);
28
+ }
29
+ }
30
+ function toonHeader(emoji, text) {
31
+ const separator = "─".repeat(40);
32
+ return `\n ${emoji} ${text}\n ${separator}`;
33
+ }
34
+ // ── Message formatting utilities ─────────────────────────────────────
35
+ /** Decode common HTML entities to plain text. */
36
+ function decodeHtmlEntities(text) {
37
+ return text
38
+ .replace(/ /g, " ")
39
+ .replace(/"/g, '"')
40
+ .replace(/&/g, "&")
41
+ .replace(/&lt;/g, "<")
42
+ .replace(/&gt;/g, ">")
43
+ .replace(/&#8203;/g, "") // zero-width space
44
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)));
45
+ }
46
+ /** Strip HTML tags and decode entities from message content. */
47
+ function cleanContent(content) {
48
+ return decodeHtmlEntities(content.replace(/<[^>]*>/g, "")).trim();
49
+ }
50
+ /** Extract quoted text from HTML blockquotes, returning quote and body separately. */
51
+ function extractQuote(content) {
52
+ for (const tag of ["blockquote", "quote"]) {
53
+ const pattern = new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, "i");
54
+ const match = content.match(pattern);
55
+ if (match) {
56
+ const quote = cleanContent(match[0]);
57
+ const remainder = content.replace(pattern, "");
58
+ return { quote: quote || null, body: cleanContent(remainder) };
59
+ }
60
+ }
61
+ return { quote: null, body: cleanContent(content) };
62
+ }
63
+ /** Build a map from message ID to sender display name. */
64
+ function buildSenderLookup(messages) {
65
+ const lookup = new Map();
66
+ for (const message of messages) {
67
+ lookup.set(message.id, message.senderDisplayName || "(system)");
68
+ }
69
+ return lookup;
70
+ }
71
+ // ── Shared conversation resolution ───────────────────────────────────
72
+ /**
73
+ * Resolve a conversation ID from the standard identification parameters.
74
+ *
75
+ * Supports three ways to identify a conversation:
76
+ * 1. conversationId — direct thread ID
77
+ * 2. chat — topic name (partial match via findConversation)
78
+ * 3. to — person name (1:1 lookup via findOneOnOneConversation)
79
+ *
80
+ * Returns both the resolved ID and a human-readable label.
81
+ */
82
+ async function resolveConversationId(client, parameters) {
83
+ const conversationId = parameters.conversationId;
84
+ const chat = parameters.chat;
85
+ const to = parameters.to;
86
+ if (conversationId) {
87
+ return { conversationId, label: conversationId };
88
+ }
89
+ if (chat) {
90
+ const conversation = await client.findConversation(chat);
91
+ if (!conversation) {
92
+ throw new Error(`No conversation matching "${chat}" found.`);
93
+ }
94
+ return { conversationId: conversation.id, label: conversation.topic };
95
+ }
96
+ if (to) {
97
+ const result = await client.findOneOnOneConversation(to);
98
+ if (!result) {
99
+ throw new Error(`No 1:1 conversation found with "${to}".`);
100
+ }
101
+ return {
102
+ conversationId: result.conversationId,
103
+ label: result.memberDisplayName,
104
+ };
105
+ }
106
+ throw new Error("One of --conversation-id, --chat, or --to is required.");
107
+ }
108
+ /** Shared parameter definitions for conversation identification. */
109
+ const conversationParameters = [
110
+ {
111
+ name: "chat",
112
+ type: "string",
113
+ description: "Find conversation by topic name (partial match)",
114
+ required: false,
115
+ },
116
+ {
117
+ name: "to",
118
+ type: "string",
119
+ description: "Find 1:1 conversation by person name",
120
+ required: false,
121
+ },
122
+ {
123
+ name: "conversationId",
124
+ type: "string",
125
+ description: "Direct conversation thread ID",
126
+ required: false,
127
+ },
128
+ ];
129
+ // ── Action definitions ───────────────────────────────────────────────
130
+ const listConversations = {
131
+ name: "list-conversations",
132
+ title: "List Teams Conversations",
133
+ description: "List conversations (chats, group chats, meetings, channels). " +
134
+ "Returns conversation ID, topic, type, member count, and last message time.",
135
+ parameters: [
136
+ {
137
+ name: "limit",
138
+ type: "number",
139
+ description: "Maximum number of conversations to return",
140
+ required: false,
141
+ default: 50,
142
+ },
143
+ ],
144
+ execute: async (client, parameters) => {
145
+ const limit = parameters.limit ?? 50;
146
+ return client.listConversations({ pageSize: limit });
147
+ },
148
+ formatResult: (result) => {
149
+ const conversations = result;
150
+ const lines = [`\n${conversations.length} conversations:\n`];
151
+ for (let i = 0; i < conversations.length; i++) {
152
+ const conversation = conversations[i];
153
+ const lastMessage = conversation.lastMessageTime?.slice(0, 10) ?? "unknown";
154
+ const topic = conversation.topic || "(untitled 1:1 chat)";
155
+ lines.push(` [${i}] ${conversation.threadType}: "${topic}" ` +
156
+ `(members: ${conversation.memberCount ?? "?"}, last: ${lastMessage})`);
157
+ }
158
+ return lines.join("\n");
159
+ },
160
+ formatMarkdown: (result) => {
161
+ const conversations = result;
162
+ const lines = [`## Conversations (${conversations.length})`, ""];
163
+ if (conversations.length === 0)
164
+ return lines.join("\n");
165
+ lines.push("| # | Topic | Type | Members | Last Message |");
166
+ lines.push("|---|-------|------|---------|--------------|");
167
+ for (let i = 0; i < conversations.length; i++) {
168
+ const conversation = conversations[i];
169
+ const lastMessage = conversation.lastMessageTime?.slice(0, 10) ?? "unknown";
170
+ const topic = conversation.topic || "(untitled 1:1 chat)";
171
+ lines.push(`| ${i} | ${topic} | ${conversation.threadType} | ${conversation.memberCount ?? "?"} | ${lastMessage} |`);
172
+ }
173
+ return lines.join("\n");
174
+ },
175
+ formatToon: (result) => {
176
+ const conversations = result;
177
+ const lines = [toonHeader("📋", `${conversations.length} Conversations`)];
178
+ for (let i = 0; i < conversations.length; i++) {
179
+ const conversation = conversations[i];
180
+ const lastMessage = conversation.lastMessageTime?.slice(0, 10) ?? "unknown";
181
+ const topic = conversation.topic || "(untitled 1:1 chat)";
182
+ lines.push("");
183
+ lines.push(` 💬 [${i}] "${topic}"`);
184
+ lines.push(` ${conversation.threadType} · ${conversation.memberCount ?? "?"} members · last: ${lastMessage}`);
185
+ }
186
+ return lines.join("\n");
187
+ },
188
+ };
189
+ const findConversation = {
190
+ name: "find-conversation",
191
+ title: "Find Teams Conversation",
192
+ description: "Find a conversation by topic name (case-insensitive partial match). " +
193
+ "When Substrate search is available, also matches by member names. " +
194
+ "For 1:1 chats (which have no topic), use find-one-on-one instead.",
195
+ parameters: [
196
+ {
197
+ name: "query",
198
+ type: "string",
199
+ description: "Partial topic name to search for",
200
+ required: true,
201
+ },
202
+ ],
203
+ execute: async (client, parameters) => {
204
+ const query = parameters.query;
205
+ return client.findConversation(query);
206
+ },
207
+ formatResult: (result) => {
208
+ if (!result)
209
+ return "No conversation found.";
210
+ const conversation = result;
211
+ const lastMessage = conversation.lastMessageTime?.slice(0, 10) ?? "unknown";
212
+ return (`Found: "${conversation.topic}" ` +
213
+ `(${conversation.id}, ${conversation.threadType}, ` +
214
+ `members: ${conversation.memberCount ?? "?"}, last: ${lastMessage})`);
215
+ },
216
+ formatMarkdown: (result) => {
217
+ if (!result)
218
+ return "No conversation found.";
219
+ const conversation = result;
220
+ const lastMessage = conversation.lastMessageTime?.slice(0, 10) ?? "unknown";
221
+ return [
222
+ `## Found: "${conversation.topic}"`,
223
+ "",
224
+ `- **ID:** ${conversation.id}`,
225
+ `- **Type:** ${conversation.threadType}`,
226
+ `- **Members:** ${conversation.memberCount ?? "?"}`,
227
+ `- **Last message:** ${lastMessage}`,
228
+ ].join("\n");
229
+ },
230
+ formatToon: (result) => {
231
+ if (!result)
232
+ return "\n 🔍 No conversation found.";
233
+ const conversation = result;
234
+ const lastMessage = conversation.lastMessageTime?.slice(0, 10) ?? "unknown";
235
+ return [
236
+ toonHeader("🔍", `Found: "${conversation.topic}"`),
237
+ ` 🆔 ${conversation.id}`,
238
+ ` 📁 ${conversation.threadType} · ${conversation.memberCount ?? "?"} members · last: ${lastMessage}`,
239
+ ].join("\n");
240
+ },
241
+ };
242
+ const findOneOnOne = {
243
+ name: "find-one-on-one",
244
+ title: "Find 1:1 Conversation",
245
+ description: "Find a 1:1 conversation with a person by name. " +
246
+ "Uses Substrate people/chat search when available, " +
247
+ "falls back to scanning message senders. " +
248
+ "Also finds the self-chat if the name matches the current user.",
249
+ parameters: [
250
+ {
251
+ name: "personName",
252
+ type: "string",
253
+ description: "Name of the person to find (case-insensitive partial match)",
254
+ required: true,
255
+ },
256
+ ],
257
+ execute: async (client, parameters) => {
258
+ const personName = parameters.personName;
259
+ return client.findOneOnOneConversation(personName);
260
+ },
261
+ formatResult: (result) => {
262
+ if (!result)
263
+ return "No 1:1 conversation found.";
264
+ const searchResult = result;
265
+ return `Found 1:1 with ${searchResult.memberDisplayName} (${searchResult.conversationId})`;
266
+ },
267
+ formatMarkdown: (result) => {
268
+ if (!result)
269
+ return "No 1:1 conversation found.";
270
+ const searchResult = result;
271
+ return [
272
+ `## Found 1:1 with ${searchResult.memberDisplayName}`,
273
+ "",
274
+ `- **Conversation ID:** ${searchResult.conversationId}`,
275
+ ].join("\n");
276
+ },
277
+ formatToon: (result) => {
278
+ if (!result)
279
+ return "\n 🔍 No 1:1 conversation found.";
280
+ const searchResult = result;
281
+ return [
282
+ toonHeader("🔍", `Found 1:1 with ${searchResult.memberDisplayName}`),
283
+ ` 🆔 ${searchResult.conversationId}`,
284
+ ].join("\n");
285
+ },
286
+ };
287
+ const getMessages = {
288
+ name: "get-messages",
289
+ title: "Get Messages",
290
+ description: "Get messages from a conversation. " +
291
+ "Identify the conversation by topic name (--chat), " +
292
+ "person name for 1:1 chats (--to), or direct ID (--conversation-id). " +
293
+ "At least one identifier is required.",
294
+ parameters: [
295
+ ...conversationParameters,
296
+ {
297
+ name: "maxPages",
298
+ type: "number",
299
+ description: "Maximum pagination pages to fetch",
300
+ required: false,
301
+ default: 100,
302
+ },
303
+ {
304
+ name: "pageSize",
305
+ type: "number",
306
+ description: "Messages per page",
307
+ required: false,
308
+ default: 200,
309
+ },
310
+ {
311
+ name: "textOnly",
312
+ type: "boolean",
313
+ description: "Only return text messages, excluding system events (default: true)",
314
+ required: false,
315
+ default: true,
316
+ },
317
+ {
318
+ name: "order",
319
+ type: "string",
320
+ description: "Message order: oldest-first (chronological, default) or newest-first",
321
+ required: false,
322
+ default: "oldest-first",
323
+ },
324
+ ],
325
+ execute: async (client, parameters) => {
326
+ const { conversationId } = await resolveConversationId(client, parameters);
327
+ const maxPages = parameters.maxPages ?? 100;
328
+ const pageSize = parameters.pageSize ?? 200;
329
+ const textOnly = parameters.textOnly ?? true;
330
+ const onProgress = parameters.onProgress;
331
+ let messages = await client.getMessages(conversationId, {
332
+ maxPages,
333
+ pageSize,
334
+ onProgress,
335
+ });
336
+ if (textOnly) {
337
+ messages = messages.filter((message) => (message.messageType === "RichText/Html" ||
338
+ message.messageType === "Text") &&
339
+ !message.isDeleted);
340
+ }
341
+ const order = parameters.order ?? "oldest-first";
342
+ if (order === "oldest-first") {
343
+ messages = [...messages].reverse();
344
+ }
345
+ return messages;
346
+ },
347
+ formatResult: (result) => {
348
+ const messages = result;
349
+ const senderLookup = buildSenderLookup(messages);
350
+ const lines = [`\n${messages.length} messages:\n`];
351
+ for (const message of messages) {
352
+ const time = message.originalArrivalTime.slice(0, 19).replace("T", " ");
353
+ const sender = message.senderDisplayName || "(system)";
354
+ const { quote, body } = extractQuote(message.content);
355
+ if (quote && message.quotedMessageId) {
356
+ const quotedSender = senderLookup.get(message.quotedMessageId) ?? "unknown";
357
+ lines.push(` [${time}] ${sender}:`);
358
+ lines.push(` > [replying to ${quotedSender}]: ${quote.slice(0, 80)}`);
359
+ lines.push(` ${body.slice(0, 120)}`);
360
+ }
361
+ else {
362
+ lines.push(` [${time}] ${sender}: ${body.slice(0, 120)}`);
363
+ }
364
+ }
365
+ return lines.join("\n");
366
+ },
367
+ formatMarkdown: (result) => {
368
+ const messages = result;
369
+ const senderLookup = buildSenderLookup(messages);
370
+ const lines = [`## Messages (${messages.length})`, ""];
371
+ let previousSender = "";
372
+ for (const message of messages) {
373
+ const time = message.originalArrivalTime.slice(0, 19).replace("T", " ");
374
+ const sender = message.senderDisplayName || "(system)";
375
+ const { quote, body } = extractQuote(message.content);
376
+ if (sender === previousSender) {
377
+ lines.push(`*${time}*`, "");
378
+ }
379
+ else {
380
+ lines.push(`### ${sender} — ${time}`, "");
381
+ previousSender = sender;
382
+ }
383
+ if (quote && message.quotedMessageId) {
384
+ const quotedSender = senderLookup.get(message.quotedMessageId) ?? "unknown";
385
+ lines.push(`> **[replying to ${quotedSender}]:** ${quote}`, "");
386
+ }
387
+ lines.push(body, "");
388
+ }
389
+ return lines.join("\n");
390
+ },
391
+ formatToon: (result) => {
392
+ const messages = result;
393
+ const senderLookup = buildSenderLookup(messages);
394
+ const lines = [toonHeader("💬", `${messages.length} Messages`)];
395
+ let previousSender = "";
396
+ for (const message of messages) {
397
+ const time = message.originalArrivalTime.slice(0, 19).replace("T", " ");
398
+ const sender = message.senderDisplayName || "(system)";
399
+ const { quote, body } = extractQuote(message.content);
400
+ lines.push("");
401
+ if (sender === previousSender) {
402
+ lines.push(` ${time}`);
403
+ }
404
+ else {
405
+ lines.push(` 🗣️ ${sender} · ${time}`);
406
+ previousSender = sender;
407
+ }
408
+ if (quote && message.quotedMessageId) {
409
+ const quotedSender = senderLookup.get(message.quotedMessageId) ?? "unknown";
410
+ lines.push(` > [replying to ${quotedSender}]: ${quote.slice(0, 80)}`);
411
+ }
412
+ lines.push(` ${body.slice(0, 120)}`);
413
+ }
414
+ return lines.join("\n");
415
+ },
416
+ };
417
+ const sendMessage = {
418
+ name: "send-message",
419
+ title: "Send Message",
420
+ description: "Send a message to a conversation. " +
421
+ "Identify the conversation by topic name (--chat), " +
422
+ "person name for 1:1 chats (--to), or direct ID (--conversation-id). " +
423
+ "At least one identifier is required. " +
424
+ "Content is interpreted as Markdown by default and converted to rich HTML.",
425
+ parameters: [
426
+ ...conversationParameters,
427
+ {
428
+ name: "content",
429
+ type: "string",
430
+ description: "Message content to send",
431
+ required: true,
432
+ },
433
+ {
434
+ name: "messageFormat",
435
+ type: "string",
436
+ description: 'Content format: "markdown" (default, converted to HTML), "html" (raw HTML), or "text" (plain text)',
437
+ required: false,
438
+ default: "markdown",
439
+ },
440
+ ],
441
+ execute: async (client, parameters) => {
442
+ const { conversationId, label } = await resolveConversationId(client, parameters);
443
+ const content = parameters.content;
444
+ const messageFormat = parameters.messageFormat ?? "markdown";
445
+ const result = await client.sendMessage(conversationId, content, messageFormat);
446
+ return { ...result, conversation: label };
447
+ },
448
+ formatResult: (result) => {
449
+ const { messageId, arrivalTime, conversation } = result;
450
+ return [
451
+ `Message sent to "${conversation}"`,
452
+ ` Message ID: ${messageId}`,
453
+ ` Arrival time: ${arrivalTime}`,
454
+ ].join("\n");
455
+ },
456
+ formatMarkdown: (result) => {
457
+ const { messageId, arrivalTime, conversation } = result;
458
+ return [
459
+ "## Message Sent",
460
+ "",
461
+ `- **To:** ${conversation}`,
462
+ `- **Message ID:** ${messageId}`,
463
+ `- **Arrival time:** ${arrivalTime}`,
464
+ ].join("\n");
465
+ },
466
+ formatToon: (result) => {
467
+ const { messageId, arrivalTime, conversation } = result;
468
+ return [
469
+ toonHeader("✅", "Message Sent!"),
470
+ ` 📨 To: "${conversation}"`,
471
+ ` 🆔 ${messageId}`,
472
+ ` ⏰ ${arrivalTime}`,
473
+ ].join("\n");
474
+ },
475
+ };
476
+ const getMembers = {
477
+ name: "get-members",
478
+ title: "Get Conversation Members",
479
+ description: "List members of a conversation. " +
480
+ "Identify the conversation by topic name (--chat), " +
481
+ "person name for 1:1 chats (--to), or direct ID (--conversation-id). " +
482
+ "At least one identifier is required. " +
483
+ "Display names are resolved via the Teams profile API when available, with message history as fallback.",
484
+ parameters: [...conversationParameters],
485
+ execute: async (client, parameters) => {
486
+ const { conversationId } = await resolveConversationId(client, parameters);
487
+ return client.getMembers(conversationId);
488
+ },
489
+ formatResult: (result) => {
490
+ const members = result;
491
+ const people = members.filter((member) => member.memberType === "person");
492
+ const bots = members.filter((member) => member.memberType === "bot");
493
+ const lines = [`\n${people.length} people, ${bots.length} bots:\n`];
494
+ for (const member of people) {
495
+ const name = member.displayName || "(unknown)";
496
+ lines.push(` ${name} (${member.role}) — ${member.id}`);
497
+ }
498
+ if (bots.length > 0) {
499
+ lines.push("");
500
+ lines.push(" Bots/Apps:");
501
+ for (const bot of bots) {
502
+ const name = bot.displayName || "(unnamed bot)";
503
+ lines.push(` ${name} — ${bot.id}`);
504
+ }
505
+ }
506
+ return lines.join("\n");
507
+ },
508
+ formatMarkdown: (result) => {
509
+ const members = result;
510
+ const people = members.filter((member) => member.memberType === "person");
511
+ const bots = members.filter((member) => member.memberType === "bot");
512
+ const lines = [
513
+ `## Members (${people.length} people, ${bots.length} bots)`,
514
+ "",
515
+ ];
516
+ if (people.length > 0) {
517
+ lines.push("| Name | Role | ID |");
518
+ lines.push("|------|------|----|");
519
+ for (const member of people) {
520
+ const name = member.displayName || "(unknown)";
521
+ lines.push(`| ${name} | ${member.role} | ${member.id} |`);
522
+ }
523
+ }
524
+ if (bots.length > 0) {
525
+ lines.push("", "### Bots/Apps", "");
526
+ lines.push("| Name | ID |");
527
+ lines.push("|------|----|");
528
+ for (const bot of bots) {
529
+ const name = bot.displayName || "(unnamed bot)";
530
+ lines.push(`| ${name} | ${bot.id} |`);
531
+ }
532
+ }
533
+ return lines.join("\n");
534
+ },
535
+ formatToon: (result) => {
536
+ const members = result;
537
+ const people = members.filter((member) => member.memberType === "person");
538
+ const bots = members.filter((member) => member.memberType === "bot");
539
+ const lines = [
540
+ toonHeader("👥", `${people.length} People, ${bots.length} Bots`),
541
+ ];
542
+ for (const member of people) {
543
+ const name = member.displayName || "(unknown)";
544
+ lines.push("");
545
+ lines.push(` 👤 ${name} · ${member.role}`);
546
+ lines.push(` ${member.id}`);
547
+ }
548
+ if (bots.length > 0) {
549
+ lines.push("");
550
+ lines.push(" 🤖 Bots/Apps:");
551
+ for (const bot of bots) {
552
+ const name = bot.displayName || "(unnamed bot)";
553
+ lines.push(` 🤖 ${name} — ${bot.id}`);
554
+ }
555
+ }
556
+ return lines.join("\n");
557
+ },
558
+ };
559
+ const whoami = {
560
+ name: "whoami",
561
+ title: "Current User Info",
562
+ description: "Get the display name and region of the currently authenticated user.",
563
+ parameters: [],
564
+ execute: async (client) => {
565
+ const displayName = await client.getCurrentUserDisplayName();
566
+ const token = client.getToken();
567
+ return { displayName, region: token.region };
568
+ },
569
+ formatResult: (result) => {
570
+ const { displayName, region } = result;
571
+ return `${displayName} (region: ${region})`;
572
+ },
573
+ formatMarkdown: (result) => {
574
+ const { displayName, region } = result;
575
+ return [`## ${displayName}`, "", `- **Region:** ${region}`].join("\n");
576
+ },
577
+ formatToon: (result) => {
578
+ const { displayName, region } = result;
579
+ return [toonHeader("🙋", displayName), ` 📍 region: ${region}`].join("\n");
580
+ },
581
+ };
582
+ // ── Transcript action ────────────────────────────────────────────────
583
+ /** Format a VTT timestamp (HH:MM:SS.mmm) as a compact time string. */
584
+ function formatTimestamp(timestamp) {
585
+ // Strip leading "00:" hours if zero, and trim milliseconds
586
+ const withoutMilliseconds = timestamp.replace(/\.\d+$/, "");
587
+ return withoutMilliseconds.replace(/^00:/, "");
588
+ }
589
+ /** Group consecutive entries by the same speaker for more readable output. */
590
+ function groupBySpeaker(entries) {
591
+ const groups = [];
592
+ for (const entry of entries) {
593
+ const lastGroup = groups[groups.length - 1];
594
+ if (lastGroup && lastGroup.speaker === entry.speaker) {
595
+ lastGroup.segments.push(entry.text);
596
+ }
597
+ else {
598
+ groups.push({
599
+ speaker: entry.speaker,
600
+ startTime: entry.startTime,
601
+ segments: [entry.text],
602
+ });
603
+ }
604
+ }
605
+ return groups;
606
+ }
607
+ const getTranscript = {
608
+ name: "get-transcript",
609
+ title: "Get Meeting Transcript",
610
+ description: "Get the meeting transcript from a conversation that contains a recorded meeting. " +
611
+ "Identify the conversation by topic name (--chat), " +
612
+ "person name for 1:1 chats (--to), or direct ID (--conversation-id). " +
613
+ "Use --raw-vtt to get the original VTT file instead of parsed output.",
614
+ parameters: [
615
+ ...conversationParameters,
616
+ {
617
+ name: "rawVtt",
618
+ type: "boolean",
619
+ description: "Return the original VTT file content instead of parsed transcript (default: false)",
620
+ required: false,
621
+ default: false,
622
+ },
623
+ ],
624
+ execute: async (client, parameters) => {
625
+ const { conversationId } = await resolveConversationId(client, parameters);
626
+ const rawVtt = parameters.rawVtt ?? false;
627
+ const transcriptResult = await client.getTranscript(conversationId);
628
+ if (rawVtt) {
629
+ return { rawVtt: transcriptResult.rawVtt, format: "vtt" };
630
+ }
631
+ return transcriptResult;
632
+ },
633
+ formatResult: (result) => {
634
+ const data = result;
635
+ if ("format" in data && data.format === "vtt") {
636
+ return data.rawVtt;
637
+ }
638
+ const transcript = data;
639
+ const groups = groupBySpeaker(transcript.entries);
640
+ const lines = [
641
+ `\nTranscript: ${transcript.meetingTitle} (${transcript.entries.length} segments)\n`,
642
+ ];
643
+ for (const group of groups) {
644
+ const time = formatTimestamp(group.startTime);
645
+ lines.push(` [${time}] ${group.speaker}:`);
646
+ lines.push(` ${group.segments.join(" ")}`);
647
+ }
648
+ return lines.join("\n");
649
+ },
650
+ formatMarkdown: (result) => {
651
+ const data = result;
652
+ if ("format" in data && data.format === "vtt") {
653
+ return ["```vtt", data.rawVtt, "```"].join("\n");
654
+ }
655
+ const transcript = data;
656
+ const groups = groupBySpeaker(transcript.entries);
657
+ const lines = [
658
+ `## Transcript: ${transcript.meetingTitle}`,
659
+ "",
660
+ `*${transcript.entries.length} segments*`,
661
+ "",
662
+ ];
663
+ for (const group of groups) {
664
+ const time = formatTimestamp(group.startTime);
665
+ lines.push(`**${group.speaker}** *(${time})*`, "");
666
+ lines.push(group.segments.join(" "), "");
667
+ }
668
+ return lines.join("\n");
669
+ },
670
+ formatToon: (result) => {
671
+ const data = result;
672
+ if ("format" in data && data.format === "vtt") {
673
+ return data.rawVtt;
674
+ }
675
+ const transcript = data;
676
+ const groups = groupBySpeaker(transcript.entries);
677
+ const lines = [
678
+ toonHeader("🎙️", `Transcript: ${transcript.meetingTitle} (${transcript.entries.length} segments)`),
679
+ ];
680
+ for (const group of groups) {
681
+ const time = formatTimestamp(group.startTime);
682
+ lines.push("");
683
+ lines.push(` 🗣️ ${group.speaker} · ${time}`);
684
+ lines.push(` ${group.segments.join(" ")}`);
685
+ }
686
+ return lines.join("\n");
687
+ },
688
+ };
689
+ // ── find-people ──────────────────────────────────────────────────────
690
+ const findPeopleAction = {
691
+ name: "find-people",
692
+ title: "Find People",
693
+ description: "Search for people in the organization directory by name. " +
694
+ "Uses the Substrate search API (requires authentication via auto-login or interactive). " +
695
+ "Returns matching people with emails, job titles, and departments.",
696
+ parameters: [
697
+ {
698
+ name: "query",
699
+ type: "string",
700
+ description: "Name or partial name to search for",
701
+ required: true,
702
+ },
703
+ {
704
+ name: "maxResults",
705
+ type: "number",
706
+ description: "Maximum results to return (default: 10)",
707
+ required: false,
708
+ default: 10,
709
+ },
710
+ ],
711
+ execute: async (client, parameters) => {
712
+ const query = parameters.query;
713
+ const maxResults = parameters.maxResults ?? 10;
714
+ return client.findPeople(query, maxResults);
715
+ },
716
+ formatResult: (result) => {
717
+ const people = result;
718
+ if (people.length === 0)
719
+ return "No people found.";
720
+ return people
721
+ .map((person) => `${person.displayName} <${person.email}> — ${person.jobTitle || "no title"}, ${person.department || "no department"}`)
722
+ .join("\n");
723
+ },
724
+ formatMarkdown: (result) => {
725
+ const people = result;
726
+ if (people.length === 0)
727
+ return "No people found.";
728
+ const lines = [`## People (${people.length} found)`, ""];
729
+ for (const person of people) {
730
+ lines.push(`### ${person.displayName}`);
731
+ lines.push(`- **Email:** ${person.email}`);
732
+ if (person.jobTitle)
733
+ lines.push(`- **Title:** ${person.jobTitle}`);
734
+ if (person.department)
735
+ lines.push(`- **Department:** ${person.department}`);
736
+ lines.push(`- **MRI:** ${person.mri}`);
737
+ lines.push("");
738
+ }
739
+ return lines.join("\n");
740
+ },
741
+ formatToon: (result) => {
742
+ const people = result;
743
+ if (people.length === 0)
744
+ return "\n 🔍 No people found.";
745
+ const lines = [toonHeader("👥", `Found ${people.length} people`)];
746
+ for (const person of people) {
747
+ lines.push(` 👤 ${person.displayName}`);
748
+ lines.push(` 📧 ${person.email} · ${person.jobTitle || "?"} · ${person.department || "?"}`);
749
+ }
750
+ return lines.join("\n");
751
+ },
752
+ };
753
+ // ── find-chats ───────────────────────────────────────────────────────
754
+ const findChatsAction = {
755
+ name: "find-chats",
756
+ title: "Find Chats",
757
+ description: "Search for chats by name or member name. " +
758
+ "Uses the Substrate search API (requires authentication via auto-login or interactive). " +
759
+ "Returns matching chats with member lists and thread IDs.",
760
+ parameters: [
761
+ {
762
+ name: "query",
763
+ type: "string",
764
+ description: "Chat name or member name to search for",
765
+ required: true,
766
+ },
767
+ {
768
+ name: "maxResults",
769
+ type: "number",
770
+ description: "Maximum results to return (default: 10)",
771
+ required: false,
772
+ default: 10,
773
+ },
774
+ ],
775
+ execute: async (client, parameters) => {
776
+ const query = parameters.query;
777
+ const maxResults = parameters.maxResults ?? 10;
778
+ return client.findChats(query, maxResults);
779
+ },
780
+ formatResult: (result) => {
781
+ const chats = result;
782
+ if (chats.length === 0)
783
+ return "No chats found.";
784
+ return chats
785
+ .map((chat) => {
786
+ const name = chat.name || "(untitled)";
787
+ const members = chat.matchingMembers
788
+ .map((member) => member.displayName)
789
+ .join(", ");
790
+ return `${name} (${chat.threadType}, ${chat.totalMemberCount} members${members ? `, matched: ${members}` : ""}) — ${chat.threadId}`;
791
+ })
792
+ .join("\n");
793
+ },
794
+ formatMarkdown: (result) => {
795
+ const chats = result;
796
+ if (chats.length === 0)
797
+ return "No chats found.";
798
+ const lines = [`## Chats (${chats.length} found)`, ""];
799
+ for (const chat of chats) {
800
+ lines.push(`### ${chat.name || "(untitled)"}`);
801
+ lines.push(`- **Thread ID:** ${chat.threadId}`);
802
+ lines.push(`- **Type:** ${chat.threadType}`);
803
+ lines.push(`- **Members:** ${chat.totalMemberCount}`);
804
+ if (chat.matchingMembers.length > 0) {
805
+ lines.push(`- **Matched:** ${chat.matchingMembers.map((member) => member.displayName).join(", ")}`);
806
+ }
807
+ lines.push("");
808
+ }
809
+ return lines.join("\n");
810
+ },
811
+ formatToon: (result) => {
812
+ const chats = result;
813
+ if (chats.length === 0)
814
+ return "\n 🔍 No chats found.";
815
+ const lines = [toonHeader("💬", `Found ${chats.length} chats`)];
816
+ for (const chat of chats) {
817
+ lines.push(` 💬 ${chat.name || "(untitled)"}`);
818
+ lines.push(` 📁 ${chat.threadType} · ${chat.totalMemberCount} members`);
819
+ if (chat.matchingMembers.length > 0) {
820
+ const matched = chat.matchingMembers
821
+ .map((member) => member.displayName)
822
+ .join(", ");
823
+ lines.push(` 🎯 Matched: ${matched}`);
824
+ }
825
+ }
826
+ return lines.join("\n");
827
+ },
828
+ };
829
+ // ── Registry ─────────────────────────────────────────────────────────
830
+ exports.actions = [
831
+ listConversations,
832
+ findConversation,
833
+ findOneOnOne,
834
+ findPeopleAction,
835
+ findChatsAction,
836
+ getMessages,
837
+ sendMessage,
838
+ getMembers,
839
+ whoami,
840
+ getTranscript,
841
+ ];
842
+ //# sourceMappingURL=actions.js.map