agent-messenger 2.35.0 → 2.36.1

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.
Files changed (77) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/teams/client.d.ts +4 -4
  4. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  5. package/dist/src/platforms/teams/client.js +14 -14
  6. package/dist/src/platforms/teams/client.js.map +1 -1
  7. package/dist/src/platforms/teams/commands/chat.d.ts +2 -0
  8. package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -1
  9. package/dist/src/platforms/teams/commands/chat.js +7 -2
  10. package/dist/src/platforms/teams/commands/chat.js.map +1 -1
  11. package/dist/src/platforms/teams/commands/format.d.ts +3 -0
  12. package/dist/src/platforms/teams/commands/format.d.ts.map +1 -0
  13. package/dist/src/platforms/teams/commands/format.js +10 -0
  14. package/dist/src/platforms/teams/commands/format.js.map +1 -0
  15. package/dist/src/platforms/teams/commands/message.d.ts +1 -0
  16. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  17. package/dist/src/platforms/teams/commands/message.js +4 -1
  18. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  19. package/dist/src/platforms/teams/html-sanitizer.d.ts +17 -0
  20. package/dist/src/platforms/teams/html-sanitizer.d.ts.map +1 -0
  21. package/dist/src/platforms/teams/html-sanitizer.js +120 -0
  22. package/dist/src/platforms/teams/html-sanitizer.js.map +1 -0
  23. package/dist/src/platforms/teams/index.d.ts +1 -1
  24. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  25. package/dist/src/platforms/teams/index.js.map +1 -1
  26. package/dist/src/platforms/teams/types.d.ts +1 -0
  27. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  28. package/dist/src/platforms/teams/types.js.map +1 -1
  29. package/dist/src/platforms/telegram/tdlib.d.ts.map +1 -1
  30. package/dist/src/platforms/telegram/tdlib.js +5 -2
  31. package/dist/src/platforms/telegram/tdlib.js.map +1 -1
  32. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  33. package/dist/src/platforms/webex/client.js +1 -1
  34. package/dist/src/platforms/webex/client.js.map +1 -1
  35. package/dist/src/shared/utils/markdown-to-html.d.ts.map +1 -0
  36. package/dist/src/{platforms/webex → shared/utils}/markdown-to-html.js +0 -0
  37. package/dist/src/shared/utils/markdown-to-html.js.map +1 -0
  38. package/package.json +1 -1
  39. package/skills/agent-channeltalk/SKILL.md +1 -1
  40. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  41. package/skills/agent-discord/SKILL.md +1 -1
  42. package/skills/agent-discordbot/SKILL.md +1 -1
  43. package/skills/agent-imessage/SKILL.md +1 -1
  44. package/skills/agent-instagram/SKILL.md +1 -1
  45. package/skills/agent-kakaotalk/SKILL.md +1 -1
  46. package/skills/agent-line/SKILL.md +1 -1
  47. package/skills/agent-slack/SKILL.md +1 -1
  48. package/skills/agent-slackbot/SKILL.md +1 -1
  49. package/skills/agent-teams/SKILL.md +44 -2
  50. package/skills/agent-teams/references/common-patterns.md +2 -2
  51. package/skills/agent-telegram/SKILL.md +1 -1
  52. package/skills/agent-telegrambot/SKILL.md +1 -1
  53. package/skills/agent-webex/SKILL.md +1 -1
  54. package/skills/agent-webexbot/SKILL.md +1 -1
  55. package/skills/agent-wechatbot/SKILL.md +1 -1
  56. package/skills/agent-whatsapp/SKILL.md +1 -1
  57. package/skills/agent-whatsappbot/SKILL.md +1 -1
  58. package/src/platforms/teams/client.test.ts +127 -0
  59. package/src/platforms/teams/client.ts +27 -14
  60. package/src/platforms/teams/commands/chat.test.ts +70 -1
  61. package/src/platforms/teams/commands/chat.ts +15 -4
  62. package/src/platforms/teams/commands/format.ts +12 -0
  63. package/src/platforms/teams/commands/message.test.ts +56 -0
  64. package/src/platforms/teams/commands/message.ts +6 -2
  65. package/src/platforms/teams/html-sanitizer.test.ts +123 -0
  66. package/src/platforms/teams/html-sanitizer.ts +134 -0
  67. package/src/platforms/teams/index.ts +1 -0
  68. package/src/platforms/teams/types.ts +2 -0
  69. package/src/platforms/telegram/tdlib-node-test.ts +19 -0
  70. package/src/platforms/telegram/tdlib-node.test.ts +11 -0
  71. package/src/platforms/telegram/tdlib.ts +6 -2
  72. package/src/platforms/webex/client.ts +2 -1
  73. package/src/{platforms/webex → shared/utils}/markdown-to-html.test.ts +7 -0
  74. package/src/{platforms/webex → shared/utils}/markdown-to-html.ts +0 -0
  75. package/dist/src/platforms/webex/markdown-to-html.d.ts.map +0 -1
  76. package/dist/src/platforms/webex/markdown-to-html.js.map +0 -1
  77. /package/dist/src/{platforms/webex → shared/utils}/markdown-to-html.d.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -205,6 +205,11 @@ Output includes the authenticated user's identity information.
205
205
  agent-teams message send <team-id> <channel-id> <content>
206
206
  agent-teams message send <team-id> 19:abc123@thread.tacv2 "Hello world"
207
207
 
208
+ # Send a rich-text message written in markdown
209
+ agent-teams message send <team-id> <channel-id> "## Deploy done
210
+ - service A
211
+ - service B" --format markdown
212
+
208
213
  # List messages
209
214
  agent-teams message list <team-id> <channel-id>
210
215
  agent-teams message list <team-id> 19:abc123@thread.tacv2 --limit 50
@@ -249,15 +254,52 @@ agent-teams chat history <chat-id> --limit 100
249
254
 
250
255
  # Send a message to a chat
251
256
  agent-teams chat send <chat-id> "Hello"
257
+ agent-teams chat send <chat-id> "**Important**: see the [board](https://example.com)" --format markdown
252
258
 
253
259
  # Edit one of your own chat messages
254
260
  agent-teams chat edit <chat-id> <message-id> "Updated text"
261
+ agent-teams chat edit <chat-id> <message-id> "Fixed \`bug\`" --format markdown
255
262
  ```
256
263
 
257
264
  Chat IDs look like `19:guid1_guid2@unq.gbl.spaces` (1:1) or `19:guid@thread.tacv2` (group). Get them from `chat list`. The `48:notes` chat (`type: self`) is your personal "to me" notes thread.
258
265
 
259
266
  `chat edit` only works on your own messages, and only for chats — Teams channel messages are not editable through this API.
260
267
 
268
+ ### Message Formatting
269
+
270
+ `message send`, `chat send`, and `chat edit` accept `--format <text|markdown|html>` (default `text`).
271
+
272
+ In `text` mode the content is HTML-escaped, so `<`, `>`, and `&` are sent literally.
273
+
274
+ In `markdown` mode the content is converted to HTML before sending. Supported syntax:
275
+
276
+ | Syntax | Renders as |
277
+ | --- | --- |
278
+ | `# Heading` through `#### Heading` | headings |
279
+ | `##### Heading`, `###### Heading` | **not supported** — indistinguishable from body text |
280
+ | `**bold**`, `_italic_`, `***bold italic***` | emphasis |
281
+ | `` `inline code` `` | inline code |
282
+ | triple-backtick fence | fenced code block, without syntax highlighting |
283
+ | `- item` | bulleted list |
284
+ | `1. item` | numbered list |
285
+ | `> quote` | blockquote |
286
+ | `[label](https://example.com)` | link |
287
+ | `---` | horizontal rule |
288
+
289
+ Heading levels 5 and 6 are unusable: the Teams client renders `<h5>` and `<h6>` at body size, so they are indistinguishable from surrounding text. Only go as deep as `####`. Fenced code blocks render as monospaced blocks but are never syntax-highlighted — a language tag (` ```ts `) is accepted and then ignored. Both behaviors were verified against a live account.
290
+
291
+ Links are restricted to `http:`, `https:`, `mailto:`, root-relative (`/`), and anchor (`#`) URLs; anything else renders as plain text.
292
+
293
+ In `html` mode the content is filtered through a tag whitelist rather than sent verbatim. Use it when you need Teams-specific markup that markdown cannot express — most commonly @mentions:
294
+
295
+ ```bash
296
+ agent-teams message send <team-id> <channel-id> "Hey <at id=\"29:abc\">John</at>, build is ready" --format html
297
+ ```
298
+
299
+ Allowed tags: `at`, `a`, `b`, `i`, `u`, `s`, `strong`, `em`, `code`, `pre`, `br`, `p`, `ul`, `ol`, `li`, `blockquote`, `h1`–`h6`, `hr`. `<at>` only keeps its `id` attribute; `<a>` only keeps `href`, and only when it matches the same URL whitelist as markdown mode; every other tag keeps no attributes at all. Anything not on the list — `<script>`, `<img>`, `on*` event attributes, malformed or unrecognized tags — is escaped or stripped rather than passed through.
300
+
301
+ The whitelist limits the blast radius, but it does not make the mode safe for arbitrary input: only pass content you construct yourself. Untrusted input belongs in `text` mode.
302
+
261
303
  ### Team Commands
262
304
 
263
305
  ```bash
@@ -385,7 +427,7 @@ agent-teams channel list --pretty
385
427
  | Channel identifiers | UUID format (19:xxx@thread.tacv2) | Snowflake IDs | Channel name or ID |
386
428
  | Token storage | Cookies SQLite | LevelDB | LevelDB |
387
429
  | Token expiry | **60-90 minutes** | Rarely expires | Rarely expires |
388
- | Mentions | `<at id="user-id">Name</at>` | `<@user_id>` | `<@USER_ID>` |
430
+ | Mentions | `<at id="user-id">Name</at>` (needs `--format html`) | `<@user_id>` | `<@USER_ID>` |
389
431
 
390
432
  **Important**: Teams uses UUID-style channel IDs (like `19:abc123@thread.tacv2`). You cannot use channel names directly - use `channel list` to find IDs first.
391
433
 
@@ -279,12 +279,12 @@ if [ -z "$USER_ID" ] || [ "$USER_ID" = "null" ]; then
279
279
  fi
280
280
 
281
281
  # Send message with mention (Teams format)
282
- agent-teams message send "$TEAM_ID" "$CHANNEL_ID" "Hey <at id=\"$USER_ID\">$USER_NAME</at>, the build is ready for review!"
282
+ agent-teams message send "$TEAM_ID" "$CHANNEL_ID" "Hey <at id=\"$USER_ID\">$USER_NAME</at>, the build is ready for review!" --format html
283
283
  ```
284
284
 
285
285
  **When to use**: Notifications, task assignments, code review requests.
286
286
 
287
- **Note**: Teams mentions use format `<at id="USER_ID">Display Name</at>`.
287
+ **Note**: Teams mentions use format `<at id="USER_ID">Display Name</at>` and require `--format html` — in the default `text` mode the tags are escaped and render literally.
288
288
 
289
289
  ## Pattern 8: Reaction-Based Workflow
290
290
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegrambot
3
3
  description: Interact with Telegram using bot tokens - send messages, read chats, manage reactions
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-telegrambot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webexbot
3
3
  description: Interact with Cisco Webex using bot tokens - send messages, reply in threads, upload and download files, look up people, read spaces, manage memberships, stream real-time events
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-webexbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.35.0
4
+ version: 2.36.1
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -328,6 +328,38 @@ describe('TeamsClient', () => {
328
328
  }),
329
329
  )
330
330
  })
331
+
332
+ it('converts markdown to HTML when format is markdown', async () => {
333
+ mockResponse({ OriginalArrivalTime: 1704067200000 })
334
+
335
+ const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
336
+ const message = await client.sendChatMessage('19:1on1@unq.gbl.spaces', '**bold** and `code`', 'markdown')
337
+
338
+ // The return value echoes the caller's original markdown, not the converted HTML
339
+ expect(message.content).toBe('**bold** and `code`')
340
+ expect(fetchCalls[0].options?.body).toBe(
341
+ JSON.stringify({
342
+ content: '<strong>bold</strong> and <code>code</code>',
343
+ messagetype: 'RichText/Html',
344
+ contenttype: 'text',
345
+ }),
346
+ )
347
+ })
348
+
349
+ it('passes content through unchanged when format is html', async () => {
350
+ mockResponse({ OriginalArrivalTime: 1704067200000 })
351
+
352
+ const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
353
+ await client.sendChatMessage('19:1on1@unq.gbl.spaces', '<b>raw</b>', 'html')
354
+
355
+ expect(fetchCalls[0].options?.body).toBe(
356
+ JSON.stringify({
357
+ content: '<b>raw</b>',
358
+ messagetype: 'RichText/Html',
359
+ contenttype: 'text',
360
+ }),
361
+ )
362
+ })
331
363
  })
332
364
 
333
365
  describe('editChatMessage', () => {
@@ -352,6 +384,39 @@ describe('TeamsClient', () => {
352
384
  }),
353
385
  )
354
386
  })
387
+
388
+ it('converts markdown to HTML when format is markdown', async () => {
389
+ mockResponse({ edittime: 1704067200000 })
390
+
391
+ const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
392
+ const message = await client.editChatMessage('19:1on1@unq.gbl.spaces', 'msg1', '**bold**', 'markdown')
393
+
394
+ expect(message.content).toBe('**bold**')
395
+ expect(fetchCalls[0].options?.body).toBe(
396
+ JSON.stringify({
397
+ content: '<strong>bold</strong>',
398
+ messagetype: 'RichText/Html',
399
+ contenttype: 'text',
400
+ skypeeditedid: 'msg1',
401
+ }),
402
+ )
403
+ })
404
+
405
+ it('passes content through unchanged when format is html', async () => {
406
+ mockResponse({ edittime: 1704067200000 })
407
+
408
+ const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
409
+ await client.editChatMessage('19:1on1@unq.gbl.spaces', 'msg1', '<b>raw</b>', 'html')
410
+
411
+ expect(fetchCalls[0].options?.body).toBe(
412
+ JSON.stringify({
413
+ content: '<b>raw</b>',
414
+ messagetype: 'RichText/Html',
415
+ contenttype: 'text',
416
+ skypeeditedid: 'msg1',
417
+ }),
418
+ )
419
+ })
355
420
  })
356
421
 
357
422
  describe('getTeam', () => {
@@ -437,6 +502,68 @@ describe('TeamsClient', () => {
437
502
  expect(message.parent_message_id).toBe('root1')
438
503
  expect(message.is_thread_reply).toBe(true)
439
504
  })
505
+
506
+ it('escapes HTML in channel messages when format is text', async () => {
507
+ mockResponse({
508
+ id: 'msg1',
509
+ channel_id: 'ch1',
510
+ author: { id: '123', displayName: 'Test User' },
511
+ content: 'a &lt;b&gt; &amp; c',
512
+ timestamp: '2024-01-01T00:00:00.000Z',
513
+ })
514
+
515
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
516
+ await client.sendMessage('111', 'ch1', 'a <b> & c')
517
+
518
+ expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'a &lt;b&gt; &amp; c' }))
519
+ })
520
+
521
+ it('converts markdown to HTML for channel messages', async () => {
522
+ mockResponse({
523
+ id: 'msg1',
524
+ channel_id: 'ch1',
525
+ author: { id: '123', displayName: 'Test User' },
526
+ content: '<strong>bold</strong>',
527
+ timestamp: '2024-01-01T00:00:00.000Z',
528
+ })
529
+
530
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
531
+ await client.sendMessage('111', 'ch1', '**bold**', undefined, 'markdown')
532
+
533
+ expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: '<strong>bold</strong>' }))
534
+ })
535
+
536
+ it('converts markdown to HTML for channel thread replies', async () => {
537
+ mockResponse({
538
+ id: 'reply1',
539
+ channel_id: 'ch1',
540
+ author: { id: '123', displayName: 'Test User' },
541
+ content: '<strong>bold</strong>',
542
+ timestamp: '2024-01-01T00:01:00.000Z',
543
+ })
544
+
545
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
546
+ await client.sendMessage('111', 'ch1', '**bold**', 'root1', 'markdown')
547
+
548
+ expect(fetchCalls[0].options?.body).toBe(
549
+ JSON.stringify({ content: '<strong>bold</strong>', parentMessageId: 'root1' }),
550
+ )
551
+ })
552
+
553
+ it('passes content through unchanged when format is html', async () => {
554
+ mockResponse({
555
+ id: 'msg1',
556
+ channel_id: 'ch1',
557
+ author: { id: '123', displayName: 'Test User' },
558
+ content: 'Hey <at id="29:xyz">John</at>',
559
+ timestamp: '2024-01-01T00:00:00.000Z',
560
+ })
561
+
562
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
563
+ await client.sendMessage('111', 'ch1', 'Hey <at id="29:xyz">John</at>', undefined, 'html')
564
+
565
+ expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Hey <at id="29:xyz">John</at>' }))
566
+ })
440
567
  })
441
568
 
442
569
  describe('getMessages', () => {
@@ -2,8 +2,11 @@ import { randomUUID } from 'node:crypto'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { basename } from 'node:path'
4
4
 
5
+ import { escapeHtml, markdownToHtml } from '@/shared/utils/markdown-to-html'
6
+
5
7
  import { SUBSTRATE_SEARCH_URL } from './app-config'
6
8
  import { TeamsCredentialManager } from './credential-manager'
9
+ import { sanitizeTeamsHtml } from './html-sanitizer'
7
10
  import { TeamsTokenProvider } from './token-provider'
8
11
  import type {
9
12
  TeamsAccountType,
@@ -12,6 +15,7 @@ import type {
12
15
  TeamsChatType,
13
16
  TeamsFile,
14
17
  TeamsMessage,
18
+ TeamsMessageFormat,
15
19
  TeamsRegion,
16
20
  TeamsSearchResult,
17
21
  TeamsTeam,
@@ -230,13 +234,9 @@ async function readDownloadResponse(response: Response, codePrefix: string): Pro
230
234
  return Buffer.from(await response.arrayBuffer())
231
235
  }
232
236
 
233
- function escapeHtml(value: string): string {
234
- return value
235
- .replaceAll('&', '&amp;')
236
- .replaceAll('<', '&lt;')
237
- .replaceAll('>', '&gt;')
238
- .replaceAll('"', '&quot;')
239
- .replaceAll("'", '&#39;')
237
+ function formatContent(content: string, format: TeamsMessageFormat): string {
238
+ if (format === 'html') return sanitizeTeamsHtml(content)
239
+ return format === 'markdown' ? markdownToHtml(content) : escapeHtml(content)
240
240
  }
241
241
 
242
242
  function withThreadMetadata(message: RawTeamsMessage, rootMessageId?: string): TeamsMessage {
@@ -692,13 +692,13 @@ export class TeamsClient {
692
692
  }))
693
693
  }
694
694
 
695
- async sendChatMessage(chatId: string, content: string): Promise<TeamsMessage> {
695
+ async sendChatMessage(chatId: string, content: string, format: TeamsMessageFormat = 'text'): Promise<TeamsMessage> {
696
696
  interface SendResponse {
697
697
  OriginalArrivalTime?: number
698
698
  }
699
699
  const encodedChatId = encodeURIComponent(chatId)
700
700
  const response = await this.request<SendResponse>('POST', `/users/ME/conversations/${encodedChatId}/messages`, {
701
- content: escapeHtml(content),
701
+ content: formatContent(content, format),
702
702
  messagetype: 'RichText/Html',
703
703
  contenttype: 'text',
704
704
  })
@@ -713,7 +713,12 @@ export class TeamsClient {
713
713
  }
714
714
  }
715
715
 
716
- async editChatMessage(chatId: string, messageId: string, content: string): Promise<TeamsMessage> {
716
+ async editChatMessage(
717
+ chatId: string,
718
+ messageId: string,
719
+ content: string,
720
+ format: TeamsMessageFormat = 'text',
721
+ ): Promise<TeamsMessage> {
717
722
  interface EditResponse {
718
723
  edittime?: string | number
719
724
  }
@@ -724,7 +729,7 @@ export class TeamsClient {
724
729
  'PUT',
725
730
  `/users/ME/conversations/${encodedChatId}/messages/${encodedMessageId}`,
726
731
  {
727
- content: escapeHtml(content),
732
+ content: formatContent(content, format),
728
733
  messagetype: 'RichText/Html',
729
734
  contenttype: 'text',
730
735
  skypeeditedid: messageId,
@@ -758,12 +763,20 @@ export class TeamsClient {
758
763
  )
759
764
  }
760
765
 
761
- async sendMessage(teamId: string, channelId: string, content: string, rootMessageId?: string): Promise<TeamsMessage> {
766
+ async sendMessage(
767
+ teamId: string,
768
+ channelId: string,
769
+ content: string,
770
+ rootMessageId?: string,
771
+ format: TeamsMessageFormat = 'text',
772
+ ): Promise<TeamsMessage> {
773
+ const body = formatContent(content, format)
774
+
762
775
  if (rootMessageId) {
763
776
  const response = await this.request<RawTeamsMessage>(
764
777
  'POST',
765
778
  `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies`,
766
- { content, parentMessageId: rootMessageId },
779
+ { content: body, parentMessageId: rootMessageId },
767
780
  CSA_API_BASE,
768
781
  )
769
782
  return withThreadMetadata(response, rootMessageId)
@@ -772,7 +785,7 @@ export class TeamsClient {
772
785
  return this.request<TeamsMessage>(
773
786
  'POST',
774
787
  `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages`,
775
- { content },
788
+ { content: body },
776
789
  CSA_API_BASE,
777
790
  )
778
791
  }
@@ -107,6 +107,7 @@ it('send: returns sent message', async () => {
107
107
  expect(consoleSpy).toHaveBeenCalled()
108
108
  const output = consoleSpy.mock.calls[0][0]
109
109
  expect(output).toContain('Hello world')
110
+ expect(clientSendChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'Hello world', 'text')
110
111
  })
111
112
 
112
113
  it('edit: edits a chat message and returns updated content', async () => {
@@ -115,8 +116,76 @@ it('edit: edits a chat message and returns updated content', async () => {
115
116
 
116
117
  await editAction('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content', { pretty: false })
117
118
 
118
- expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content')
119
+ expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content', 'text')
119
120
  expect(consoleSpy).toHaveBeenCalled()
120
121
  const output = consoleSpy.mock.calls[0][0]
121
122
  expect(output).toContain('Edited content')
122
123
  })
124
+
125
+ it('send: passes markdown format to the client', async () => {
126
+ const consoleSpy = mock((_msg: string) => {})
127
+ console.log = consoleSpy
128
+
129
+ await sendAction('19:1on1@unq.gbl.spaces', '**bold**', { pretty: false, format: 'markdown' })
130
+
131
+ expect(clientSendChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', '**bold**', 'markdown')
132
+ })
133
+
134
+ it('send: defaults to text format when --format is omitted', async () => {
135
+ const consoleSpy = mock((_msg: string) => {})
136
+ console.log = consoleSpy
137
+
138
+ await sendAction('19:1on1@unq.gbl.spaces', 'plain', { pretty: false })
139
+
140
+ expect(clientSendChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'plain', 'text')
141
+ })
142
+
143
+ it('send: rejects an invalid format', async () => {
144
+ const consoleSpy = mock((_msg: string) => {})
145
+ console.log = consoleSpy
146
+ const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
147
+ throw new Error(`exit:${code}`)
148
+ })
149
+
150
+ try {
151
+ await expect(sendAction('19:1on1@unq.gbl.spaces', 'hi', { pretty: false, format: 'invalid' })).rejects.toThrow(
152
+ 'exit:1',
153
+ )
154
+
155
+ expect(consoleSpy.mock.calls[0][0]).toContain('Invalid format')
156
+ expect(clientSendChatMessageSpy).not.toHaveBeenCalled()
157
+ } finally {
158
+ // Restore in finally: a failing assertion above would otherwise leave
159
+ // process.exit mocked, so every later test would throw instead of exiting.
160
+ exitSpy.mockRestore()
161
+ }
162
+ })
163
+
164
+ it('edit: passes markdown format to the client', async () => {
165
+ const consoleSpy = mock((_msg: string) => {})
166
+ console.log = consoleSpy
167
+
168
+ await editAction('19:1on1@unq.gbl.spaces', 'msg_123', '**bold**', { pretty: false, format: 'markdown' })
169
+
170
+ expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', '**bold**', 'markdown')
171
+ })
172
+
173
+ it('edit: rejects an invalid format', async () => {
174
+ const consoleSpy = mock((_msg: string) => {})
175
+ console.log = consoleSpy
176
+ const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
177
+ throw new Error(`exit:${code}`)
178
+ })
179
+
180
+ try {
181
+ await expect(
182
+ editAction('19:1on1@unq.gbl.spaces', 'msg_123', 'hi', { pretty: false, format: 'invalid' }),
183
+ ).rejects.toThrow('exit:1')
184
+
185
+ expect(clientEditChatMessageSpy).not.toHaveBeenCalled()
186
+ } finally {
187
+ // Restore in finally: a failing assertion above would otherwise leave
188
+ // process.exit mocked, so every later test would throw instead of exiting.
189
+ exitSpy.mockRestore()
190
+ }
191
+ })
@@ -5,6 +5,7 @@ import { formatOutput } from '@/shared/utils/output'
5
5
 
6
6
  import { TeamsClient } from '../client'
7
7
  import { TeamsCredentialManager } from '../credential-manager'
8
+ import { resolveFormat } from './format'
8
9
 
9
10
  export async function listAction(options: { pretty?: boolean }): Promise<void> {
10
11
  try {
@@ -70,7 +71,13 @@ export async function historyAction(chatId: string, options: { limit?: number; p
70
71
  }
71
72
  }
72
73
 
73
- export async function sendAction(chatId: string, content: string, options: { pretty?: boolean }): Promise<void> {
74
+ export async function sendAction(
75
+ chatId: string,
76
+ content: string,
77
+ options: { pretty?: boolean; format?: string },
78
+ ): Promise<void> {
79
+ const format = resolveFormat(options.format, options.pretty)
80
+
74
81
  try {
75
82
  const credManager = new TeamsCredentialManager()
76
83
  const cred = await credManager.getTokenWithExpiry()
@@ -86,7 +93,7 @@ export async function sendAction(chatId: string, content: string, options: { pre
86
93
  accountType: cred.accountType,
87
94
  region: cred.region,
88
95
  })
89
- const message = await client.sendChatMessage(chatId, content)
96
+ const message = await client.sendChatMessage(chatId, content, format)
90
97
 
91
98
  const output = {
92
99
  id: message.id,
@@ -104,8 +111,10 @@ export async function editAction(
104
111
  chatId: string,
105
112
  messageId: string,
106
113
  content: string,
107
- options: { pretty?: boolean },
114
+ options: { pretty?: boolean; format?: string },
108
115
  ): Promise<void> {
116
+ const format = resolveFormat(options.format, options.pretty)
117
+
109
118
  try {
110
119
  const credManager = new TeamsCredentialManager()
111
120
  const cred = await credManager.getTokenWithExpiry()
@@ -121,7 +130,7 @@ export async function editAction(
121
130
  accountType: cred.accountType,
122
131
  region: cred.region,
123
132
  })
124
- const message = await client.editChatMessage(chatId, messageId, content)
133
+ const message = await client.editChatMessage(chatId, messageId, content, format)
125
134
 
126
135
  const output = {
127
136
  id: message.id,
@@ -161,6 +170,7 @@ export const chatCommand = new Command('chat')
161
170
  .description('Send a message to a chat')
162
171
  .argument('<chat-id>', 'Chat ID')
163
172
  .argument('<content>', 'Message content')
173
+ .option('--format <format>', 'Message format: text, markdown, or html', 'text')
164
174
  .option('--pretty', 'Pretty print JSON output')
165
175
  .action(sendAction),
166
176
  )
@@ -170,6 +180,7 @@ export const chatCommand = new Command('chat')
170
180
  .argument('<chat-id>', 'Chat ID')
171
181
  .argument('<message-id>', 'Message ID')
172
182
  .argument('<content>', 'New message content')
183
+ .option('--format <format>', 'Message format: text, markdown, or html', 'text')
173
184
  .option('--pretty', 'Pretty print JSON output')
174
185
  .action(editAction),
175
186
  )
@@ -0,0 +1,12 @@
1
+ import { formatOutput } from '@/shared/utils/output'
2
+
3
+ import type { TeamsMessageFormat } from '../types'
4
+
5
+ export function resolveFormat(value: string | undefined, pretty: boolean | undefined): TeamsMessageFormat {
6
+ const format = value ?? 'text'
7
+ if (format !== 'text' && format !== 'markdown' && format !== 'html') {
8
+ console.log(formatOutput({ error: `Invalid format: ${format}. Use "text", "markdown", or "html".` }, pretty))
9
+ process.exit(1)
10
+ }
11
+ return format
12
+ }
@@ -207,3 +207,59 @@ it('delete: returns success', async () => {
207
207
  const output = consoleSpy.mock.calls[0][0]
208
208
  expect(output).toContain('deleted')
209
209
  })
210
+
211
+ it('send: passes markdown format to the client', async () => {
212
+ const consoleSpy = mock((_msg: string) => {})
213
+ console.log = consoleSpy
214
+
215
+ await sendAction('team_123', 'ch_456', '**bold**', { pretty: false, format: 'markdown' })
216
+
217
+ expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', '**bold**', undefined, 'markdown')
218
+ })
219
+
220
+ it('send: defaults to text format when --format is omitted', async () => {
221
+ const consoleSpy = mock((_msg: string) => {})
222
+ console.log = consoleSpy
223
+
224
+ await sendAction('team_123', 'ch_456', 'plain', { pretty: false })
225
+
226
+ expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', 'plain', undefined, 'text')
227
+ })
228
+
229
+ it('send: rejects an invalid format', async () => {
230
+ const consoleSpy = mock((_msg: string) => {})
231
+ console.log = consoleSpy
232
+ const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
233
+ throw new Error(`exit:${code}`)
234
+ })
235
+
236
+ try {
237
+ await expect(sendAction('team_123', 'ch_456', 'hi', { pretty: false, format: 'invalid' })).rejects.toThrow('exit:1')
238
+
239
+ expect(consoleSpy).toHaveBeenCalled()
240
+ expect(consoleSpy.mock.calls[0][0]).toContain('Invalid format')
241
+ expect(clientSendMessageSpy).not.toHaveBeenCalled()
242
+ } finally {
243
+ // Restore in finally: a failing assertion above would otherwise leave
244
+ // process.exit mocked, so every later test would throw instead of exiting.
245
+ exitSpy.mockRestore()
246
+ }
247
+ })
248
+
249
+ it('send: passes html format to the client', async () => {
250
+ const consoleSpy = mock((_msg: string) => {})
251
+ console.log = consoleSpy
252
+
253
+ await sendAction('team_123', 'ch_456', '<at id="29:x">John</at>', { pretty: false, format: 'html' })
254
+
255
+ expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', '<at id="29:x">John</at>', undefined, 'html')
256
+ })
257
+
258
+ it('send: passes markdown format together with --thread', async () => {
259
+ const consoleSpy = mock((_msg: string) => {})
260
+ console.log = consoleSpy
261
+
262
+ await sendAction('team_123', 'ch_456', '**bold**', { pretty: false, thread: 'root_1', format: 'markdown' })
263
+
264
+ expect(clientSendMessageSpy).toHaveBeenCalledWith('team_123', 'ch_456', '**bold**', 'root_1', 'markdown')
265
+ })