agent-messenger 2.34.0 → 2.36.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.
Files changed (117) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/kakaotalk/auth/kakao-login.d.ts +2 -0
  4. package/dist/src/platforms/kakaotalk/auth/kakao-login.d.ts.map +1 -1
  5. package/dist/src/platforms/kakaotalk/auth/kakao-login.js +2 -2
  6. package/dist/src/platforms/kakaotalk/auth/kakao-login.js.map +1 -1
  7. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.d.ts +28 -0
  8. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.d.ts.map +1 -0
  9. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.js +100 -0
  10. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.js.map +1 -0
  11. package/dist/src/platforms/kakaotalk/client.d.ts +2 -0
  12. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  13. package/dist/src/platforms/kakaotalk/client.js +14 -1
  14. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  15. package/dist/src/platforms/kakaotalk/index.d.ts +3 -1
  16. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  17. package/dist/src/platforms/kakaotalk/index.js +1 -0
  18. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  19. package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts +9 -0
  20. package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts.map +1 -0
  21. package/dist/src/platforms/kakaotalk/protocol/login-response.js +32 -0
  22. package/dist/src/platforms/kakaotalk/protocol/login-response.js.map +1 -0
  23. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  24. package/dist/src/platforms/kakaotalk/protocol/session.js +29 -20
  25. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  26. package/dist/src/platforms/kakaotalk/protocol/types.d.ts +1 -0
  27. package/dist/src/platforms/kakaotalk/protocol/types.d.ts.map +1 -1
  28. package/dist/src/platforms/kakaotalk/protocol/types.js.map +1 -1
  29. package/dist/src/platforms/kakaotalk/types.d.ts +1 -0
  30. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  31. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  32. package/dist/src/platforms/teams/client.d.ts +4 -4
  33. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  34. package/dist/src/platforms/teams/client.js +14 -14
  35. package/dist/src/platforms/teams/client.js.map +1 -1
  36. package/dist/src/platforms/teams/commands/chat.d.ts +2 -0
  37. package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -1
  38. package/dist/src/platforms/teams/commands/chat.js +7 -2
  39. package/dist/src/platforms/teams/commands/chat.js.map +1 -1
  40. package/dist/src/platforms/teams/commands/format.d.ts +3 -0
  41. package/dist/src/platforms/teams/commands/format.d.ts.map +1 -0
  42. package/dist/src/platforms/teams/commands/format.js +10 -0
  43. package/dist/src/platforms/teams/commands/format.js.map +1 -0
  44. package/dist/src/platforms/teams/commands/message.d.ts +1 -0
  45. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  46. package/dist/src/platforms/teams/commands/message.js +4 -1
  47. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  48. package/dist/src/platforms/teams/html-sanitizer.d.ts +17 -0
  49. package/dist/src/platforms/teams/html-sanitizer.d.ts.map +1 -0
  50. package/dist/src/platforms/teams/html-sanitizer.js +120 -0
  51. package/dist/src/platforms/teams/html-sanitizer.js.map +1 -0
  52. package/dist/src/platforms/teams/index.d.ts +1 -1
  53. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  54. package/dist/src/platforms/teams/index.js.map +1 -1
  55. package/dist/src/platforms/teams/types.d.ts +1 -0
  56. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  57. package/dist/src/platforms/teams/types.js.map +1 -1
  58. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  59. package/dist/src/platforms/webex/client.js +1 -1
  60. package/dist/src/platforms/webex/client.js.map +1 -1
  61. package/dist/src/shared/utils/markdown-to-html.d.ts.map +1 -0
  62. package/dist/src/{platforms/webex → shared/utils}/markdown-to-html.js +0 -0
  63. package/dist/src/shared/utils/markdown-to-html.js.map +1 -0
  64. package/docs/content/docs/cli/kakaotalk.mdx +10 -0
  65. package/docs/content/docs/sdk/kakaotalk.mdx +54 -0
  66. package/package.json +1 -1
  67. package/skills/agent-channeltalk/SKILL.md +1 -1
  68. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  69. package/skills/agent-discord/SKILL.md +1 -1
  70. package/skills/agent-discordbot/SKILL.md +1 -1
  71. package/skills/agent-imessage/SKILL.md +1 -1
  72. package/skills/agent-instagram/SKILL.md +1 -1
  73. package/skills/agent-kakaotalk/SKILL.md +27 -1
  74. package/skills/agent-kakaotalk/references/authentication.md +59 -0
  75. package/skills/agent-kakaotalk/references/common-patterns.md +8 -0
  76. package/skills/agent-line/SKILL.md +1 -1
  77. package/skills/agent-slack/SKILL.md +1 -1
  78. package/skills/agent-slackbot/SKILL.md +1 -1
  79. package/skills/agent-teams/SKILL.md +44 -2
  80. package/skills/agent-teams/references/common-patterns.md +2 -2
  81. package/skills/agent-telegram/SKILL.md +1 -1
  82. package/skills/agent-telegrambot/SKILL.md +1 -1
  83. package/skills/agent-webex/SKILL.md +1 -1
  84. package/skills/agent-webexbot/SKILL.md +1 -1
  85. package/skills/agent-wechatbot/SKILL.md +1 -1
  86. package/skills/agent-whatsapp/SKILL.md +1 -1
  87. package/skills/agent-whatsappbot/SKILL.md +1 -1
  88. package/src/platforms/kakaotalk/auth/kakao-login.ts +2 -2
  89. package/src/platforms/kakaotalk/auth/oauth-refresh.test.ts +225 -0
  90. package/src/platforms/kakaotalk/auth/oauth-refresh.ts +144 -0
  91. package/src/platforms/kakaotalk/client.test.ts +55 -0
  92. package/src/platforms/kakaotalk/client.ts +19 -2
  93. package/src/platforms/kakaotalk/index.test.ts +11 -0
  94. package/src/platforms/kakaotalk/index.ts +8 -0
  95. package/src/platforms/kakaotalk/protocol/NOTICE.md +19 -4
  96. package/src/platforms/kakaotalk/protocol/login-response.test.ts +64 -0
  97. package/src/platforms/kakaotalk/protocol/login-response.ts +40 -0
  98. package/src/platforms/kakaotalk/protocol/session.ts +29 -21
  99. package/src/platforms/kakaotalk/protocol/types.ts +1 -0
  100. package/src/platforms/kakaotalk/types.ts +1 -0
  101. package/src/platforms/teams/client.test.ts +127 -0
  102. package/src/platforms/teams/client.ts +27 -14
  103. package/src/platforms/teams/commands/chat.test.ts +70 -1
  104. package/src/platforms/teams/commands/chat.ts +15 -4
  105. package/src/platforms/teams/commands/format.ts +12 -0
  106. package/src/platforms/teams/commands/message.test.ts +56 -0
  107. package/src/platforms/teams/commands/message.ts +6 -2
  108. package/src/platforms/teams/html-sanitizer.test.ts +123 -0
  109. package/src/platforms/teams/html-sanitizer.ts +134 -0
  110. package/src/platforms/teams/index.ts +1 -0
  111. package/src/platforms/teams/types.ts +2 -0
  112. package/src/platforms/webex/client.ts +2 -1
  113. package/src/{platforms/webex → shared/utils}/markdown-to-html.test.ts +7 -0
  114. package/src/{platforms/webex → shared/utils}/markdown-to-html.ts +0 -0
  115. package/dist/src/platforms/webex/markdown-to-html.d.ts.map +0 -1
  116. package/dist/src/platforms/webex/markdown-to-html.js.map +0 -1
  117. /package/dist/src/{platforms/webex → shared/utils}/markdown-to-html.d.ts +0 -0
@@ -15,6 +15,7 @@ import {
15
15
  getLocoDeviceConfig,
16
16
  } from './config'
17
17
  import { LocoConnection } from './connection'
18
+ import { validateLoginListResponse } from './login-response'
18
19
  import type { BookingResponse, CheckinResponse, LoginListResponse, LocoPacket, SyncState } from './types'
19
20
 
20
21
  // LOCO opcode string emitted on the wire for typing indicator pulses.
@@ -81,27 +82,34 @@ export class LocoSession {
81
82
  const lastTokenId = syncState ? new Long(syncState.lastTokenId.low, syncState.lastTokenId.high) : Long.fromNumber(0)
82
83
  const lbk = syncState?.lbk ?? 0
83
84
 
84
- const response = await this.connection.sendPacket('LOGINLIST', {
85
- appVer: deviceConfig.appVersion,
86
- prtVer: PROTOCOL_VERSION,
87
- os: deviceConfig.os,
88
- lang: LANG,
89
- dtype: DTYPE,
90
- duuid: deviceUuid,
91
- oauthToken,
92
- ntype: 0,
93
- MCCMNC: MCCMNC,
94
- revision: syncState?.revision ?? 0,
95
- chatIds,
96
- maxIds,
97
- lastTokenId,
98
- lbk,
99
- rp: new Binary(Buffer.from([0x00, 0x00, 0xff, 0xff, 0x00, 0x00])),
100
- bg: false,
101
- })
102
-
103
- this.startPing()
104
- return response.body as unknown as LoginListResponse
85
+ try {
86
+ const response = await this.connection.sendPacket('LOGINLIST', {
87
+ appVer: deviceConfig.appVersion,
88
+ prtVer: PROTOCOL_VERSION,
89
+ os: deviceConfig.os,
90
+ lang: LANG,
91
+ dtype: DTYPE,
92
+ duuid: deviceUuid,
93
+ oauthToken,
94
+ ntype: 0,
95
+ MCCMNC: MCCMNC,
96
+ revision: syncState?.revision ?? 0,
97
+ chatIds,
98
+ maxIds,
99
+ lastTokenId,
100
+ lbk,
101
+ rp: new Binary(Buffer.from([0x00, 0x00, 0xff, 0xff, 0x00, 0x00])),
102
+ bg: false,
103
+ })
104
+
105
+ const loginResult = validateLoginListResponse(response)
106
+ this.startPing()
107
+ return loginResult
108
+ } catch (error) {
109
+ this.connection.close()
110
+ this.connection = null
111
+ throw error
112
+ }
105
113
  }
106
114
 
107
115
  private async bookAndCheckin(
@@ -36,6 +36,7 @@ export interface SyncState {
36
36
 
37
37
  // LOGINLIST uses short BSON field names: c=chatId, t=type, a=activeMembers, etc.
38
38
  export interface LoginListResponse extends ChatListResponse {
39
+ status?: number
39
40
  userId: number
40
41
  revision: number
41
42
  }
@@ -12,6 +12,7 @@ export interface ExtractedKakaoToken {
12
12
  }
13
13
 
14
14
  export type KakaoAuthMethod = 'login' | 'extract'
15
+ export type KakaoAuthErrorCode = 'invalid_access_token' | 'login_rejected'
15
16
 
16
17
  export interface KakaoAccountCredentials {
17
18
  account_id: string
@@ -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
+ })
@@ -7,13 +7,16 @@ import { TeamsClient } from '../client'
7
7
  import { TeamsCredentialManager } from '../credential-manager'
8
8
  import type { TeamsMessage } from '../types'
9
9
  import { TeamsAuthCapabilityError } from '../types'
10
+ import { resolveFormat } from './format'
10
11
 
11
12
  export async function sendAction(
12
13
  teamId: string,
13
14
  channelId: string,
14
15
  content: string,
15
- options: { pretty?: boolean; thread?: string },
16
+ options: { pretty?: boolean; thread?: string; format?: string },
16
17
  ): Promise<void> {
18
+ const format = resolveFormat(options.format, options.pretty)
19
+
17
20
  try {
18
21
  const credManager = new TeamsCredentialManager()
19
22
  const cred = await credManager.getTokenWithExpiry()
@@ -29,7 +32,7 @@ export async function sendAction(
29
32
  accountType: cred.accountType,
30
33
  region: cred.region,
31
34
  })
32
- const message = await client.sendMessage(teamId, channelId, content, options.thread)
35
+ const message = await client.sendMessage(teamId, channelId, content, options.thread, format)
33
36
 
34
37
  const output = {
35
38
  id: message.id,
@@ -274,6 +277,7 @@ export const messageCommand = new Command('message')
274
277
  .argument('<channel-id>', 'Channel ID')
275
278
  .argument('<content>', 'Message content')
276
279
  .option('--thread <message-id>', 'Reply to a thread root message')
280
+ .option('--format <format>', 'Message format: text, markdown, or html', 'text')
277
281
  .option('--pretty', 'Pretty print JSON output')
278
282
  .action(sendAction),
279
283
  )