agent-messenger 2.29.0 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +27 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/discord/client.d.ts +1 -0
  5. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  6. package/dist/src/platforms/discord/client.js +3 -0
  7. package/dist/src/platforms/discord/client.js.map +1 -1
  8. package/dist/src/platforms/discord/commands/message.d.ts +3 -0
  9. package/dist/src/platforms/discord/commands/message.d.ts.map +1 -1
  10. package/dist/src/platforms/discord/commands/message.js +30 -0
  11. package/dist/src/platforms/discord/commands/message.js.map +1 -1
  12. package/dist/src/platforms/discordbot/commands/message.d.ts +1 -1
  13. package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
  14. package/dist/src/platforms/discordbot/commands/message.js +4 -4
  15. package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
  16. package/dist/src/platforms/slack/commands/message.js +4 -4
  17. package/dist/src/platforms/slack/commands/message.js.map +1 -1
  18. package/dist/src/platforms/slackbot/commands/message.js +4 -4
  19. package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
  20. package/dist/src/platforms/teams/client.d.ts +2 -0
  21. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  22. package/dist/src/platforms/teams/client.js +35 -0
  23. package/dist/src/platforms/teams/client.js.map +1 -1
  24. package/dist/src/platforms/teams/commands/chat.d.ts +3 -0
  25. package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -1
  26. package/dist/src/platforms/teams/commands/chat.js +34 -1
  27. package/dist/src/platforms/teams/commands/chat.js.map +1 -1
  28. package/dist/src/platforms/teams/index.d.ts +1 -1
  29. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  30. package/dist/src/platforms/teams/index.js.map +1 -1
  31. package/dist/src/platforms/teams/listener.d.ts +5 -0
  32. package/dist/src/platforms/teams/listener.d.ts.map +1 -1
  33. package/dist/src/platforms/teams/listener.js +54 -4
  34. package/dist/src/platforms/teams/listener.js.map +1 -1
  35. package/dist/src/platforms/teams/trouter.d.ts +7 -0
  36. package/dist/src/platforms/teams/trouter.d.ts.map +1 -1
  37. package/dist/src/platforms/teams/trouter.js +85 -0
  38. package/dist/src/platforms/teams/trouter.js.map +1 -1
  39. package/dist/src/platforms/teams/types.d.ts +9 -0
  40. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  41. package/dist/src/platforms/teams/types.js.map +1 -1
  42. package/dist/src/platforms/telegram/client.d.ts +1 -0
  43. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  44. package/dist/src/platforms/telegram/client.js +21 -0
  45. package/dist/src/platforms/telegram/client.js.map +1 -1
  46. package/dist/src/platforms/telegram/commands/message.d.ts.map +1 -1
  47. package/dist/src/platforms/telegram/commands/message.js +25 -1
  48. package/dist/src/platforms/telegram/commands/message.js.map +1 -1
  49. package/dist/src/platforms/telegrambot/commands/message.d.ts +1 -1
  50. package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -1
  51. package/dist/src/platforms/telegrambot/commands/message.js +3 -3
  52. package/dist/src/platforms/telegrambot/commands/message.js.map +1 -1
  53. package/dist/src/platforms/whatsapp/client.d.ts +3 -1
  54. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  55. package/dist/src/platforms/whatsapp/client.js +19 -3
  56. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  57. package/dist/src/platforms/whatsapp/commands/message.d.ts.map +1 -1
  58. package/dist/src/platforms/whatsapp/commands/message.js +18 -0
  59. package/dist/src/platforms/whatsapp/commands/message.js.map +1 -1
  60. package/docs/content/docs/cli/discordbot.mdx +2 -2
  61. package/docs/content/docs/cli/slack.mdx +2 -2
  62. package/docs/content/docs/cli/slackbot.mdx +2 -2
  63. package/docs/content/docs/cli/teams.mdx +15 -1
  64. package/docs/content/docs/cli/telegrambot.mdx +1 -1
  65. package/docs/content/docs/sdk/teams.mdx +30 -0
  66. package/e2e/README.md +3 -3
  67. package/e2e/discordbot.e2e.test.ts +2 -2
  68. package/e2e/slack.e2e.test.ts +2 -2
  69. package/e2e/slackbot.e2e.test.ts +2 -2
  70. package/e2e/telegrambot.e2e.test.ts +2 -2
  71. package/package.json +1 -1
  72. package/skills/agent-channeltalk/SKILL.md +1 -1
  73. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  74. package/skills/agent-discord/SKILL.md +5 -1
  75. package/skills/agent-discordbot/SKILL.md +3 -3
  76. package/skills/agent-discordbot/references/common-patterns.md +2 -2
  77. package/skills/agent-imessage/SKILL.md +1 -1
  78. package/skills/agent-instagram/SKILL.md +1 -1
  79. package/skills/agent-kakaotalk/SKILL.md +1 -1
  80. package/skills/agent-line/SKILL.md +1 -1
  81. package/skills/agent-slack/SKILL.md +3 -3
  82. package/skills/agent-slack/references/common-patterns.md +2 -2
  83. package/skills/agent-slackbot/SKILL.md +3 -3
  84. package/skills/agent-slackbot/references/common-patterns.md +4 -4
  85. package/skills/agent-teams/SKILL.md +30 -2
  86. package/skills/agent-telegram/SKILL.md +4 -1
  87. package/skills/agent-telegrambot/SKILL.md +2 -2
  88. package/skills/agent-webex/SKILL.md +1 -1
  89. package/skills/agent-webexbot/SKILL.md +1 -1
  90. package/skills/agent-wechatbot/SKILL.md +1 -1
  91. package/skills/agent-whatsapp/SKILL.md +25 -1
  92. package/skills/agent-whatsappbot/SKILL.md +1 -1
  93. package/src/platforms/discord/client.test.ts +22 -0
  94. package/src/platforms/discord/client.ts +4 -0
  95. package/src/platforms/discord/commands/message.test.ts +26 -1
  96. package/src/platforms/discord/commands/message.ts +41 -0
  97. package/src/platforms/discordbot/commands/message.test.ts +6 -6
  98. package/src/platforms/discordbot/commands/message.ts +4 -4
  99. package/src/platforms/slack/commands/message.test.ts +4 -4
  100. package/src/platforms/slack/commands/message.ts +4 -4
  101. package/src/platforms/slackbot/commands/message.ts +4 -4
  102. package/src/platforms/teams/client.test.ts +24 -0
  103. package/src/platforms/teams/client.ts +56 -0
  104. package/src/platforms/teams/commands/chat.test.ts +23 -1
  105. package/src/platforms/teams/commands/chat.ts +44 -0
  106. package/src/platforms/teams/index.ts +1 -0
  107. package/src/platforms/teams/listener.test.ts +243 -0
  108. package/src/platforms/teams/listener.ts +59 -3
  109. package/src/platforms/teams/trouter.test.ts +60 -0
  110. package/src/platforms/teams/trouter.ts +95 -0
  111. package/src/platforms/teams/types.ts +10 -0
  112. package/src/platforms/telegram/client.test.ts +60 -0
  113. package/src/platforms/telegram/client.ts +24 -0
  114. package/src/platforms/telegram/commands/message.test.ts +38 -0
  115. package/src/platforms/telegram/commands/message.ts +39 -0
  116. package/src/platforms/telegrambot/commands/message.ts +3 -3
  117. package/src/platforms/whatsapp/client.test.ts +58 -0
  118. package/src/platforms/whatsapp/client.ts +23 -3
  119. package/src/platforms/whatsapp/commands/message.test.ts +43 -0
  120. package/src/platforms/whatsapp/commands/message.ts +25 -0
@@ -246,6 +246,28 @@ describe('DiscordClient', () => {
246
246
  })
247
247
  })
248
248
 
249
+ describe('editMessage', () => {
250
+ it('edits message with PATCH', async () => {
251
+ mockResponse({
252
+ id: 'msg1',
253
+ channel_id: 'ch1',
254
+ author: { id: '123', username: 'bot' },
255
+ content: 'Updated content',
256
+ timestamp: '2024-01-01T00:00:00.000Z',
257
+ edited_timestamp: '2024-01-01T00:05:00.000Z',
258
+ })
259
+
260
+ const client = await new DiscordClient().login({ token: 'test-token' })
261
+ const message = await client.editMessage('ch1', 'msg1', 'Updated content')
262
+
263
+ expect(message.content).toBe('Updated content')
264
+ expect(message.edited_timestamp).toBe('2024-01-01T00:05:00.000Z')
265
+ expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/channels/ch1/messages/msg1')
266
+ expect(fetchCalls[0].options?.method).toBe('PATCH')
267
+ expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Updated content' }))
268
+ })
269
+ })
270
+
249
271
  describe('deleteMessage', () => {
250
272
  it('deletes message', async () => {
251
273
  mockResponse(null, 204)
@@ -242,6 +242,10 @@ export class DiscordClient {
242
242
  return this.request<DiscordMessage>('GET', `/channels/${channelId}/messages/${messageId}`)
243
243
  }
244
244
 
245
+ async editMessage(channelId: string, messageId: string, content: string): Promise<DiscordMessage> {
246
+ return this.request<DiscordMessage>('PATCH', `/channels/${channelId}/messages/${messageId}`, { content })
247
+ }
248
+
245
249
  async deleteMessage(channelId: string, messageId: string): Promise<void> {
246
250
  return this.request<void>('DELETE', `/channels/${channelId}/messages/${messageId}`)
247
251
  }
@@ -2,11 +2,12 @@ import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
2
2
 
3
3
  import { DiscordClient } from '../client'
4
4
  import { DiscordCredentialManager } from '../credential-manager'
5
- import { ackAction, deleteAction, getAction, listAction, searchAction, sendAction } from './message'
5
+ import { ackAction, deleteAction, editAction, getAction, listAction, searchAction, sendAction } from './message'
6
6
 
7
7
  let clientSendMessageSpy: ReturnType<typeof spyOn>
8
8
  let clientGetMessagesSpy: ReturnType<typeof spyOn>
9
9
  let clientGetMessageSpy: ReturnType<typeof spyOn>
10
+ let clientEditMessageSpy: ReturnType<typeof spyOn>
10
11
  let clientDeleteMessageSpy: ReturnType<typeof spyOn>
11
12
  let clientAckMessageSpy: ReturnType<typeof spyOn>
12
13
  let clientSearchMessagesSpy: ReturnType<typeof spyOn>
@@ -47,6 +48,15 @@ beforeEach(() => {
47
48
  timestamp: '2025-01-29T10:00:00Z',
48
49
  })
49
50
 
51
+ clientEditMessageSpy = spyOn(DiscordClient.prototype, 'editMessage').mockResolvedValue({
52
+ id: 'msg_123',
53
+ channel_id: 'ch_456',
54
+ author: { id: 'user_789', username: 'testuser' },
55
+ content: 'Updated content',
56
+ timestamp: '2025-01-29T10:00:00Z',
57
+ edited_timestamp: '2025-01-29T10:05:00Z',
58
+ })
59
+
50
60
  clientDeleteMessageSpy = spyOn(DiscordClient.prototype, 'deleteMessage').mockResolvedValue(undefined)
51
61
 
52
62
  clientAckMessageSpy = spyOn(DiscordClient.prototype, 'ackMessage').mockResolvedValue(undefined)
@@ -87,6 +97,7 @@ afterEach(() => {
87
97
  clientSendMessageSpy?.mockRestore()
88
98
  clientGetMessagesSpy?.mockRestore()
89
99
  clientGetMessageSpy?.mockRestore()
100
+ clientEditMessageSpy?.mockRestore()
90
101
  clientDeleteMessageSpy?.mockRestore()
91
102
  clientAckMessageSpy?.mockRestore()
92
103
  clientSearchMessagesSpy?.mockRestore()
@@ -127,6 +138,20 @@ it('get: returns single message', async () => {
127
138
  expect(output).toContain('msg_123')
128
139
  })
129
140
 
141
+ it('edit: returns updated message', async () => {
142
+ const consoleSpy = mock((_msg: string) => {})
143
+ console.log = consoleSpy
144
+
145
+ await editAction('ch_456', 'msg_123', 'Updated content', { pretty: false })
146
+
147
+ expect(clientEditMessageSpy).toHaveBeenCalledWith('ch_456', 'msg_123', 'Updated content')
148
+ expect(consoleSpy).toHaveBeenCalled()
149
+ const output = consoleSpy.mock.calls[0][0]
150
+ expect(output).toContain('msg_123')
151
+ expect(output).toContain('Updated content')
152
+ expect(output).toContain('edited_timestamp')
153
+ })
154
+
130
155
  it('delete: returns success', async () => {
131
156
  const consoleSpy = mock((_msg: string) => {})
132
157
  console.log = consoleSpy
@@ -121,6 +121,38 @@ export async function deleteAction(
121
121
  }
122
122
  }
123
123
 
124
+ export async function editAction(
125
+ channelId: string,
126
+ messageId: string,
127
+ content: string,
128
+ options: { pretty?: boolean },
129
+ ): Promise<void> {
130
+ try {
131
+ const credManager = new DiscordCredentialManager()
132
+ const config = await credManager.load()
133
+
134
+ if (!config.token) {
135
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
136
+ process.exit(1)
137
+ }
138
+
139
+ const client = await new DiscordClient().login({ token: config.token })
140
+ const message = await client.editMessage(channelId, messageId, content)
141
+
142
+ const output = {
143
+ id: message.id,
144
+ content: message.content,
145
+ author: message.author.username,
146
+ timestamp: message.timestamp,
147
+ edited_timestamp: message.edited_timestamp,
148
+ }
149
+
150
+ console.log(formatOutput(output, options.pretty))
151
+ } catch (error) {
152
+ handleError(error as Error)
153
+ }
154
+ }
155
+
124
156
  export async function ackAction(channelId: string, messageId: string, options: { pretty?: boolean }): Promise<void> {
125
157
  try {
126
158
  const credManager = new DiscordCredentialManager()
@@ -244,6 +276,15 @@ export const messageCommand = new Command('message')
244
276
  .option('--pretty', 'Pretty print JSON output')
245
277
  .action(deleteAction),
246
278
  )
279
+ .addCommand(
280
+ new Command('edit')
281
+ .description('Edit a message')
282
+ .argument('<channel-id>', 'Channel ID')
283
+ .argument('<message-id>', 'Message ID')
284
+ .argument('<content>', 'New message content')
285
+ .option('--pretty', 'Pretty print JSON output')
286
+ .action(editAction),
287
+ )
247
288
  .addCommand(
248
289
  new Command('ack')
249
290
  .description('Mark message as read (acknowledge)')
@@ -82,7 +82,7 @@ mock.module('../client', () => ({
82
82
  }))
83
83
 
84
84
  import { DiscordBotCredentialManager } from '../credential-manager'
85
- import { deleteAction, getAction, listAction, repliesAction, sendAction, updateAction } from './message'
85
+ import { deleteAction, editAction, getAction, listAction, repliesAction, sendAction } from './message'
86
86
 
87
87
  describe('message commands', () => {
88
88
  let tempDir: string
@@ -243,9 +243,9 @@ describe('message commands', () => {
243
243
  })
244
244
  })
245
245
 
246
- describe('updateAction', () => {
247
- it('updates a message', async () => {
248
- const result = await updateAction('general', 'msg1', 'updated text', { _credManager: manager })
246
+ describe('editAction', () => {
247
+ it('edits a message', async () => {
248
+ const result = await editAction('general', 'msg1', 'updated text', { _credManager: manager })
249
249
 
250
250
  expect(result.id).toBe('msg1')
251
251
  expect(result.content).toBe('updated text')
@@ -254,7 +254,7 @@ describe('message commands', () => {
254
254
  })
255
255
 
256
256
  it('resolves channel name', async () => {
257
- await updateAction('general', 'msg1', 'new', { _credManager: manager })
257
+ await editAction('general', 'msg1', 'new', { _credManager: manager })
258
258
 
259
259
  expect(mockResolveChannel).toHaveBeenCalledWith('guild1', 'general')
260
260
  })
@@ -262,7 +262,7 @@ describe('message commands', () => {
262
262
  it('returns error on failure', async () => {
263
263
  mockEditMessage.mockImplementationOnce(() => Promise.reject(new Error('Cannot edit')))
264
264
 
265
- const result = await updateAction('general', 'msg1', 'new', { _credManager: manager })
265
+ const result = await editAction('general', 'msg1', 'new', { _credManager: manager })
266
266
 
267
267
  expect(result.error).toContain('Cannot edit')
268
268
  })
@@ -95,7 +95,7 @@ export async function getAction(channel: string, messageId: string, options: Bot
95
95
  }
96
96
  }
97
97
 
98
- export async function updateAction(
98
+ export async function editAction(
99
99
  channel: string,
100
100
  messageId: string,
101
101
  text: string,
@@ -207,8 +207,8 @@ export const messageCommand = new Command('message')
207
207
  }),
208
208
  )
209
209
  .addCommand(
210
- new Command('update')
211
- .description('Update a message')
210
+ new Command('edit')
211
+ .description('Edit a message')
212
212
  .argument('<channel>', 'Channel ID or name')
213
213
  .argument('<message-id>', 'Message ID')
214
214
  .argument('<text>', 'New message text')
@@ -216,7 +216,7 @@ export const messageCommand = new Command('message')
216
216
  .option('--server <id>', 'Server ID')
217
217
  .option('--pretty', 'Pretty print JSON output')
218
218
  .action(async (channel: string, messageId: string, text: string, opts: BotOption) => {
219
- cliOutput(await updateAction(channel, messageId, text, opts), opts.pretty)
219
+ cliOutput(await editAction(channel, messageId, text, opts), opts.pretty)
220
220
  }),
221
221
  )
222
222
  .addCommand(
@@ -165,17 +165,17 @@ describe('Message Commands', () => {
165
165
  })
166
166
  })
167
167
 
168
- describe('message update', () => {
169
- it('updates message text', async () => {
168
+ describe('message edit', () => {
169
+ it('edits message text', async () => {
170
170
  // Given: A channel, message ts, and new text
171
171
  const channel = 'C123'
172
172
  const ts = '1234567890.123456'
173
173
  const newText = 'Updated message'
174
174
 
175
- // When: Updating message
175
+ // When: Editing message
176
176
  const result = await mockClient.updateMessage(channel, ts, newText)
177
177
 
178
- // Then: Should return updated message
178
+ // Then: Should return edited message
179
179
  expect(result.ts).toBe(ts)
180
180
  expect(result.text).toBe(newText)
181
181
  })
@@ -114,7 +114,7 @@ async function getAction(channelInput: string, ts: string, options: { pretty?: b
114
114
  }
115
115
  }
116
116
 
117
- async function updateAction(
117
+ async function editAction(
118
118
  channelInput: string,
119
119
  ts: string,
120
120
  text: string,
@@ -413,13 +413,13 @@ export const messageCommand = new Command('message')
413
413
  .action(getAction),
414
414
  )
415
415
  .addCommand(
416
- new Command('update')
417
- .description('Update message text')
416
+ new Command('edit')
417
+ .description('Edit message text')
418
418
  .argument('<channel>', 'Channel ID or name')
419
419
  .argument('<ts>', 'Message timestamp')
420
420
  .argument('<text>', 'New message text')
421
421
  .option('--pretty', 'Pretty print JSON output')
422
- .action(updateAction),
422
+ .action(editAction),
423
423
  )
424
424
  .addCommand(
425
425
  new Command('delete')
@@ -59,7 +59,7 @@ async function getAction(channelInput: string, ts: string, options: BotOption):
59
59
  }
60
60
  }
61
61
 
62
- async function updateAction(channelInput: string, ts: string, text: string, options: BotOption): Promise<void> {
62
+ async function editAction(channelInput: string, ts: string, text: string, options: BotOption): Promise<void> {
63
63
  try {
64
64
  const client = await getClient(options)
65
65
  const channel = await client.resolveChannel(channelInput)
@@ -158,14 +158,14 @@ export const messageCommand = new Command('message')
158
158
  .action(getAction),
159
159
  )
160
160
  .addCommand(
161
- new Command('update')
162
- .description('Update a message')
161
+ new Command('edit')
162
+ .description('Edit a message')
163
163
  .argument('<channel>', 'Channel ID or name')
164
164
  .argument('<ts>', 'Message timestamp')
165
165
  .argument('<text>', 'New message text')
166
166
  .option('--bot <id>', 'Use specific bot')
167
167
  .option('--pretty', 'Pretty print JSON output')
168
- .action(updateAction),
168
+ .action(editAction),
169
169
  )
170
170
  .addCommand(
171
171
  new Command('delete')
@@ -330,6 +330,30 @@ describe('TeamsClient', () => {
330
330
  })
331
331
  })
332
332
 
333
+ describe('editChatMessage', () => {
334
+ it('PUTs an HTML-escaped edit to a chat message', async () => {
335
+ mockResponse({ edittime: 1704067200000 })
336
+
337
+ const client = await new TeamsClient().login({ token: 'test-token', accountType: 'personal' })
338
+ const message = await client.editChatMessage('19:1on1@unq.gbl.spaces', 'msg1', 'a <b> & c')
339
+
340
+ expect(message.id).toBe('msg1')
341
+ expect(message.content).toBe('a <b> & c')
342
+ expect(fetchCalls[0].url).toBe(
343
+ 'https://msgapi.teams.live.com/v1/users/ME/conversations/19%3A1on1%40unq.gbl.spaces/messages/msg1',
344
+ )
345
+ expect(fetchCalls[0].options?.method).toBe('PUT')
346
+ expect(fetchCalls[0].options?.body).toBe(
347
+ JSON.stringify({
348
+ content: 'a &lt;b&gt; &amp; c',
349
+ messagetype: 'RichText/Html',
350
+ contenttype: 'text',
351
+ skypeeditedid: 'msg1',
352
+ }),
353
+ )
354
+ })
355
+ })
356
+
333
357
  describe('getTeam', () => {
334
358
  it('returns team info', async () => {
335
359
  mockResponse({ id: '111', name: 'Test Team', description: 'A test team' })
@@ -589,6 +589,34 @@ export class TeamsClient {
589
589
  return Array.from(teamsMap.values())
590
590
  }
591
591
 
592
+ // Realtime messages only carry a conversation id; a channel's parent teamId
593
+ // (== groupId) lives on the conversation, so the listener resolves it through
594
+ // this channelId -> teamId map.
595
+ async buildChannelTeamMap(): Promise<Map<string, string>> {
596
+ interface Conversation {
597
+ id: string
598
+ threadProperties?: {
599
+ groupId?: string
600
+ productThreadType?: string
601
+ threadType?: string
602
+ }
603
+ }
604
+ interface ConversationsResponse {
605
+ conversations: Conversation[]
606
+ }
607
+ const data = await this.request<ConversationsResponse>('GET', '/users/ME/conversations')
608
+
609
+ const channelToTeam = new Map<string, string>()
610
+ for (const conv of data.conversations ?? []) {
611
+ const tp = conv.threadProperties
612
+ if (!tp?.groupId) continue
613
+ if (!tp.productThreadType?.includes('Teams') && tp.threadType !== 'space') continue
614
+ channelToTeam.set(conv.id, tp.groupId)
615
+ }
616
+
617
+ return channelToTeam
618
+ }
619
+
592
620
  async listChats(): Promise<TeamsChat[]> {
593
621
  interface ConversationMessage {
594
622
  content?: string
@@ -685,6 +713,34 @@ export class TeamsClient {
685
713
  }
686
714
  }
687
715
 
716
+ async editChatMessage(chatId: string, messageId: string, content: string): Promise<TeamsMessage> {
717
+ interface EditResponse {
718
+ edittime?: string | number
719
+ }
720
+ const encodedChatId = encodeURIComponent(chatId)
721
+ const encodedMessageId = encodeURIComponent(messageId)
722
+ // Skype messaging backend requires skypeeditedid to duplicate the URL message id.
723
+ const response = await this.request<EditResponse>(
724
+ 'PUT',
725
+ `/users/ME/conversations/${encodedChatId}/messages/${encodedMessageId}`,
726
+ {
727
+ content: escapeHtml(content),
728
+ messagetype: 'RichText/Html',
729
+ contenttype: 'text',
730
+ skypeeditedid: messageId,
731
+ },
732
+ )
733
+
734
+ const editTime = response?.edittime
735
+ return {
736
+ id: messageId,
737
+ channel_id: chatId,
738
+ author: { id: 'ME', displayName: 'Me' },
739
+ content,
740
+ timestamp: editTime ? new Date(Number(editTime) || editTime).toISOString() : new Date().toISOString(),
741
+ }
742
+ }
743
+
688
744
  async getTeam(teamId: string): Promise<TeamsTeam> {
689
745
  return this.request<TeamsTeam>('GET', `/csa/api/v1/teams/${teamId}`, undefined, CSA_API_BASE)
690
746
  }
@@ -2,11 +2,12 @@ import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
2
2
 
3
3
  import { TeamsClient } from '../client'
4
4
  import { TeamsCredentialManager } from '../credential-manager'
5
- import { historyAction, listAction, sendAction } from './chat'
5
+ import { editAction, historyAction, listAction, sendAction } from './chat'
6
6
 
7
7
  let clientListChatsSpy: ReturnType<typeof spyOn>
8
8
  let clientGetChatMessagesSpy: ReturnType<typeof spyOn>
9
9
  let clientSendChatMessageSpy: ReturnType<typeof spyOn>
10
+ let clientEditChatMessageSpy: ReturnType<typeof spyOn>
10
11
  let credManagerLoadSpy: ReturnType<typeof spyOn>
11
12
  const originalConsoleLog = console.log
12
13
 
@@ -34,6 +35,14 @@ beforeEach(() => {
34
35
  timestamp: '2025-01-29T10:00:00Z',
35
36
  })
36
37
 
38
+ clientEditChatMessageSpy = spyOn(TeamsClient.prototype, 'editChatMessage').mockResolvedValue({
39
+ id: 'msg_123',
40
+ channel_id: '19:1on1@unq.gbl.spaces',
41
+ author: { id: 'ME', displayName: 'Me' },
42
+ content: 'Edited content',
43
+ timestamp: '2025-01-29T10:05:00Z',
44
+ })
45
+
37
46
  credManagerLoadSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue({
38
47
  current_account: 'personal',
39
48
  accounts: {
@@ -51,6 +60,7 @@ afterEach(() => {
51
60
  clientListChatsSpy?.mockRestore()
52
61
  clientGetChatMessagesSpy?.mockRestore()
53
62
  clientSendChatMessageSpy?.mockRestore()
63
+ clientEditChatMessageSpy?.mockRestore()
54
64
  credManagerLoadSpy?.mockRestore()
55
65
  console.log = originalConsoleLog
56
66
  })
@@ -98,3 +108,15 @@ it('send: returns sent message', async () => {
98
108
  const output = consoleSpy.mock.calls[0][0]
99
109
  expect(output).toContain('Hello world')
100
110
  })
111
+
112
+ it('edit: edits a chat message and returns updated content', async () => {
113
+ const consoleSpy = mock((_msg: string) => {})
114
+ console.log = consoleSpy
115
+
116
+ await editAction('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content', { pretty: false })
117
+
118
+ expect(clientEditChatMessageSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 'msg_123', 'Edited content')
119
+ expect(consoleSpy).toHaveBeenCalled()
120
+ const output = consoleSpy.mock.calls[0][0]
121
+ expect(output).toContain('Edited content')
122
+ })
@@ -100,6 +100,41 @@ export async function sendAction(chatId: string, content: string, options: { pre
100
100
  }
101
101
  }
102
102
 
103
+ export async function editAction(
104
+ chatId: string,
105
+ messageId: string,
106
+ content: string,
107
+ options: { pretty?: boolean },
108
+ ): Promise<void> {
109
+ try {
110
+ const credManager = new TeamsCredentialManager()
111
+ const cred = await credManager.getTokenWithExpiry()
112
+
113
+ if (!cred) {
114
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
115
+ process.exit(1)
116
+ }
117
+
118
+ const client = await new TeamsClient().login({
119
+ token: cred.token,
120
+ tokenExpiresAt: cred.tokenExpiresAt,
121
+ accountType: cred.accountType,
122
+ region: cred.region,
123
+ })
124
+ const message = await client.editChatMessage(chatId, messageId, content)
125
+
126
+ const output = {
127
+ id: message.id,
128
+ content: message.content,
129
+ timestamp: message.timestamp,
130
+ }
131
+
132
+ console.log(formatOutput(output, options.pretty))
133
+ } catch (error) {
134
+ handleError(error as Error)
135
+ }
136
+ }
137
+
103
138
  export const chatCommand = new Command('chat')
104
139
  .description('Chat commands (1:1, group, and self chats)')
105
140
  .addCommand(
@@ -129,3 +164,12 @@ export const chatCommand = new Command('chat')
129
164
  .option('--pretty', 'Pretty print JSON output')
130
165
  .action(sendAction),
131
166
  )
167
+ .addCommand(
168
+ new Command('edit')
169
+ .description('Edit a message in a chat (your own messages only)')
170
+ .argument('<chat-id>', 'Chat ID')
171
+ .argument('<message-id>', 'Message ID')
172
+ .argument('<content>', 'New message content')
173
+ .option('--pretty', 'Pretty print JSON output')
174
+ .action(editAction),
175
+ )
@@ -20,6 +20,7 @@ export type {
20
20
  TeamsCredentials,
21
21
  TeamsFile,
22
22
  TeamsListenerEventMap,
23
+ TeamsMention,
23
24
  TeamsMessage,
24
25
  TeamsRealtimeMessage,
25
26
  TeamsReaction,