agent-messenger 2.19.5 → 2.20.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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/line/client.d.ts +4 -0
  4. package/dist/src/platforms/line/client.d.ts.map +1 -1
  5. package/dist/src/platforms/line/client.js +124 -24
  6. package/dist/src/platforms/line/client.js.map +1 -1
  7. package/dist/src/platforms/line/e2ee-storage.d.ts +16 -0
  8. package/dist/src/platforms/line/e2ee-storage.d.ts.map +1 -0
  9. package/dist/src/platforms/line/e2ee-storage.js +93 -0
  10. package/dist/src/platforms/line/e2ee-storage.js.map +1 -0
  11. package/dist/src/platforms/teams/cli.d.ts.map +1 -1
  12. package/dist/src/platforms/teams/cli.js +2 -1
  13. package/dist/src/platforms/teams/cli.js.map +1 -1
  14. package/dist/src/platforms/teams/client.d.ts +4 -1
  15. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  16. package/dist/src/platforms/teams/client.js +84 -0
  17. package/dist/src/platforms/teams/client.js.map +1 -1
  18. package/dist/src/platforms/teams/commands/chat.d.ts +13 -0
  19. package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -0
  20. package/dist/src/platforms/teams/commands/chat.js +111 -0
  21. package/dist/src/platforms/teams/commands/chat.js.map +1 -0
  22. package/dist/src/platforms/teams/commands/index.d.ts +1 -0
  23. package/dist/src/platforms/teams/commands/index.d.ts.map +1 -1
  24. package/dist/src/platforms/teams/commands/index.js +1 -0
  25. package/dist/src/platforms/teams/commands/index.js.map +1 -1
  26. package/dist/src/platforms/teams/types.d.ts +24 -0
  27. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  28. package/dist/src/platforms/teams/types.js +8 -0
  29. package/dist/src/platforms/teams/types.js.map +1 -1
  30. package/dist/src/tui/adapters/line-adapter.js +1 -1
  31. package/dist/src/tui/adapters/line-adapter.js.map +1 -1
  32. package/docs/content/docs/cli/line.mdx +13 -11
  33. package/package.json +1 -1
  34. package/skills/agent-channeltalk/SKILL.md +1 -1
  35. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  36. package/skills/agent-discord/SKILL.md +1 -1
  37. package/skills/agent-discordbot/SKILL.md +1 -1
  38. package/skills/agent-instagram/SKILL.md +1 -1
  39. package/skills/agent-kakaotalk/SKILL.md +1 -1
  40. package/skills/agent-line/SKILL.md +7 -5
  41. package/skills/agent-line/references/common-patterns.md +5 -2
  42. package/skills/agent-slack/SKILL.md +1 -1
  43. package/skills/agent-slackbot/SKILL.md +1 -1
  44. package/skills/agent-teams/SKILL.md +20 -2
  45. package/skills/agent-teams/references/common-patterns.md +28 -0
  46. package/skills/agent-telegram/SKILL.md +1 -1
  47. package/skills/agent-telegrambot/SKILL.md +1 -1
  48. package/skills/agent-webex/SKILL.md +1 -1
  49. package/skills/agent-wechatbot/SKILL.md +1 -1
  50. package/skills/agent-whatsapp/SKILL.md +1 -1
  51. package/skills/agent-whatsappbot/SKILL.md +1 -1
  52. package/src/platforms/line/client.test.ts +223 -20
  53. package/src/platforms/line/client.ts +141 -29
  54. package/src/platforms/line/e2ee-storage.test.ts +154 -0
  55. package/src/platforms/line/e2ee-storage.ts +119 -0
  56. package/src/platforms/teams/cli.ts +2 -0
  57. package/src/platforms/teams/client.test.ts +96 -0
  58. package/src/platforms/teams/client.ts +133 -0
  59. package/src/platforms/teams/commands/chat.test.ts +100 -0
  60. package/src/platforms/teams/commands/chat.ts +131 -0
  61. package/src/platforms/teams/commands/index.ts +1 -0
  62. package/src/platforms/teams/types.ts +20 -0
  63. package/src/tui/adapters/line-adapter.ts +1 -1
@@ -5,6 +5,8 @@ import { TeamsCredentialManager } from './credential-manager'
5
5
  import type {
6
6
  TeamsAccountType,
7
7
  TeamsChannel,
8
+ TeamsChat,
9
+ TeamsChatType,
8
10
  TeamsFile,
9
11
  TeamsMessage,
10
12
  TeamsRegion,
@@ -25,6 +27,41 @@ const BASE_BACKOFF_MS = 100
25
27
  const DEFAULT_REGION: TeamsRegion = 'amer'
26
28
  const REGIONS: TeamsRegion[] = ['amer', 'emea', 'apac']
27
29
 
30
+ function stripHtml(content: string | undefined): string | undefined {
31
+ if (content === undefined) return undefined
32
+ const stripped = content.replace(/<[^>]*>/g, '')
33
+ return stripped
34
+ .replace(/&amp;/g, '&')
35
+ .replace(/&lt;/g, '<')
36
+ .replace(/&gt;/g, '>')
37
+ .replace(/&quot;/g, '"')
38
+ .replace(/&#39;/g, "'")
39
+ .replace(/\s+/g, ' ')
40
+ .trim()
41
+ }
42
+
43
+ function escapeHtml(value: string): string {
44
+ return value
45
+ .replaceAll('&', '&amp;')
46
+ .replaceAll('<', '&lt;')
47
+ .replaceAll('>', '&gt;')
48
+ .replaceAll('"', '&quot;')
49
+ .replaceAll("'", '&#39;')
50
+ }
51
+
52
+ // groupId => Teams/channel thread (handled by listTeams). "48:notes"/
53
+ // streamofnotes => the user's self ("to me") chat. Anything else without a
54
+ // non-chat threadType is a normal 1:1 (no topic) or group (has topic) chat.
55
+ function classifyChat(
56
+ id: string,
57
+ tp?: { topic?: string; threadType?: string; groupId?: string },
58
+ ): TeamsChatType | null {
59
+ if (tp?.groupId) return null
60
+ if (id === '48:notes' || tp?.threadType === 'streamofnotes') return 'self'
61
+ if (tp?.threadType && tp.threadType !== 'chat') return null
62
+ return tp?.topic ? 'group' : 'oneOnOne'
63
+ }
64
+
28
65
  export class TeamsClient {
29
66
  private token: string | null = null
30
67
  private tokenExpiresAt?: Date
@@ -324,6 +361,102 @@ export class TeamsClient {
324
361
  return Array.from(teamsMap.values())
325
362
  }
326
363
 
364
+ async listChats(): Promise<TeamsChat[]> {
365
+ interface ConversationMessage {
366
+ content?: string
367
+ composetime?: string
368
+ originalarrivaltime?: string
369
+ }
370
+ interface Conversation {
371
+ id: string
372
+ threadProperties?: {
373
+ topic?: string
374
+ threadType?: string
375
+ groupId?: string
376
+ }
377
+ lastMessage?: ConversationMessage
378
+ }
379
+ interface ConversationsResponse {
380
+ conversations: Conversation[]
381
+ }
382
+ const data = await this.request<ConversationsResponse>(
383
+ 'GET',
384
+ '/users/ME/conversations?view=msnp24Equivalent&pageSize=500',
385
+ )
386
+
387
+ const chats: TeamsChat[] = []
388
+ for (const conv of data.conversations ?? []) {
389
+ const type = classifyChat(conv.id, conv.threadProperties)
390
+ if (!type) continue
391
+
392
+ chats.push({
393
+ id: conv.id,
394
+ type,
395
+ topic: conv.threadProperties?.topic,
396
+ last_message: stripHtml(conv.lastMessage?.content),
397
+ last_message_at: conv.lastMessage?.composetime ?? conv.lastMessage?.originalarrivaltime,
398
+ })
399
+ }
400
+
401
+ return chats
402
+ }
403
+
404
+ async getChatMessages(chatId: string, limit: number = 50): Promise<TeamsMessage[]> {
405
+ interface ChatMessage {
406
+ id: string
407
+ content?: string
408
+ from?: string
409
+ imdisplayname?: string
410
+ composetime?: string
411
+ originalarrivaltime?: string
412
+ messagetype?: string
413
+ }
414
+ interface MessagesResponse {
415
+ messages: ChatMessage[]
416
+ }
417
+ const encodedChatId = encodeURIComponent(chatId)
418
+ const data = await this.request<MessagesResponse>(
419
+ 'GET',
420
+ `/users/ME/conversations/${encodedChatId}/messages?startTime=0&view=msnp24Equivalent&pageSize=${limit}`,
421
+ )
422
+
423
+ const userMessageTypes = new Set(['Text', 'RichText/Html', 'RichText/Media_CallRecording'])
424
+ return (data.messages ?? [])
425
+ .filter((msg) => !msg.messagetype || userMessageTypes.has(msg.messagetype))
426
+ .slice(0, limit)
427
+ .map((msg) => ({
428
+ id: msg.id,
429
+ channel_id: chatId,
430
+ author: {
431
+ id: msg.from ?? '',
432
+ displayName: msg.imdisplayname ?? 'Unknown',
433
+ },
434
+ content: stripHtml(msg.content) ?? '',
435
+ timestamp: msg.composetime ?? msg.originalarrivaltime ?? '',
436
+ }))
437
+ }
438
+
439
+ async sendChatMessage(chatId: string, content: string): Promise<TeamsMessage> {
440
+ interface SendResponse {
441
+ OriginalArrivalTime?: number
442
+ }
443
+ const encodedChatId = encodeURIComponent(chatId)
444
+ const response = await this.request<SendResponse>('POST', `/users/ME/conversations/${encodedChatId}/messages`, {
445
+ content: escapeHtml(content),
446
+ messagetype: 'RichText/Html',
447
+ contenttype: 'text',
448
+ })
449
+
450
+ const arrivalTime = response?.OriginalArrivalTime
451
+ return {
452
+ id: arrivalTime ? String(arrivalTime) : '',
453
+ channel_id: chatId,
454
+ author: { id: 'ME', displayName: 'Me' },
455
+ content,
456
+ timestamp: arrivalTime ? new Date(arrivalTime).toISOString() : new Date().toISOString(),
457
+ }
458
+ }
459
+
327
460
  async getTeam(teamId: string): Promise<TeamsTeam> {
328
461
  return this.request<TeamsTeam>('GET', `/csa/api/v1/teams/${teamId}`, undefined, CSA_API_BASE)
329
462
  }
@@ -0,0 +1,100 @@
1
+ import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
2
+
3
+ import { TeamsClient } from '../client'
4
+ import { TeamsCredentialManager } from '../credential-manager'
5
+ import { historyAction, listAction, sendAction } from './chat'
6
+
7
+ let clientListChatsSpy: ReturnType<typeof spyOn>
8
+ let clientGetChatMessagesSpy: ReturnType<typeof spyOn>
9
+ let clientSendChatMessageSpy: ReturnType<typeof spyOn>
10
+ let credManagerLoadSpy: ReturnType<typeof spyOn>
11
+ const originalConsoleLog = console.log
12
+
13
+ beforeEach(() => {
14
+ clientListChatsSpy = spyOn(TeamsClient.prototype, 'listChats').mockResolvedValue([
15
+ { id: '19:1on1@unq.gbl.spaces', type: 'oneOnOne', last_message: 'Hi', last_message_at: '2025-01-29T10:00:00Z' },
16
+ { id: '19:group@thread.tacv2', type: 'group', topic: 'Group Chat' },
17
+ ])
18
+
19
+ clientGetChatMessagesSpy = spyOn(TeamsClient.prototype, 'getChatMessages').mockResolvedValue([
20
+ {
21
+ id: 'msg_123',
22
+ channel_id: '19:1on1@unq.gbl.spaces',
23
+ author: { id: 'user_789', displayName: 'Alice' },
24
+ content: 'Hello world',
25
+ timestamp: '2025-01-29T10:00:00Z',
26
+ },
27
+ ])
28
+
29
+ clientSendChatMessageSpy = spyOn(TeamsClient.prototype, 'sendChatMessage').mockResolvedValue({
30
+ id: '1704067200000',
31
+ channel_id: '19:1on1@unq.gbl.spaces',
32
+ author: { id: 'ME', displayName: 'Me' },
33
+ content: 'Hello world',
34
+ timestamp: '2025-01-29T10:00:00Z',
35
+ })
36
+
37
+ credManagerLoadSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue({
38
+ current_account: 'personal',
39
+ accounts: {
40
+ personal: {
41
+ token: 'test_token',
42
+ account_type: 'personal' as const,
43
+ current_team: null,
44
+ teams: {},
45
+ },
46
+ },
47
+ })
48
+ })
49
+
50
+ afterEach(() => {
51
+ clientListChatsSpy?.mockRestore()
52
+ clientGetChatMessagesSpy?.mockRestore()
53
+ clientSendChatMessageSpy?.mockRestore()
54
+ credManagerLoadSpy?.mockRestore()
55
+ console.log = originalConsoleLog
56
+ })
57
+
58
+ it('list: returns array of chats', async () => {
59
+ const consoleSpy = mock((_msg: string) => {})
60
+ console.log = consoleSpy
61
+
62
+ await listAction({ pretty: false })
63
+
64
+ expect(consoleSpy).toHaveBeenCalled()
65
+ const output = consoleSpy.mock.calls[0][0]
66
+ expect(output).toContain('19:1on1@unq.gbl.spaces')
67
+ expect(output).toContain('19:group@thread.tacv2')
68
+ })
69
+
70
+ it('history: returns array of messages', async () => {
71
+ const consoleSpy = mock((_msg: string) => {})
72
+ console.log = consoleSpy
73
+
74
+ await historyAction('19:1on1@unq.gbl.spaces', { limit: 50, pretty: false })
75
+
76
+ expect(consoleSpy).toHaveBeenCalled()
77
+ const output = consoleSpy.mock.calls[0][0]
78
+ expect(output).toContain('msg_123')
79
+ expect(output).toContain('Alice')
80
+ })
81
+
82
+ it('history: falls back to default limit when given a non-positive value', async () => {
83
+ const consoleSpy = mock((_msg: string) => {})
84
+ console.log = consoleSpy
85
+
86
+ await historyAction('19:1on1@unq.gbl.spaces', { limit: -5, pretty: false })
87
+
88
+ expect(clientGetChatMessagesSpy).toHaveBeenCalledWith('19:1on1@unq.gbl.spaces', 50)
89
+ })
90
+
91
+ it('send: returns sent message', async () => {
92
+ const consoleSpy = mock((_msg: string) => {})
93
+ console.log = consoleSpy
94
+
95
+ await sendAction('19:1on1@unq.gbl.spaces', 'Hello world', { pretty: false })
96
+
97
+ expect(consoleSpy).toHaveBeenCalled()
98
+ const output = consoleSpy.mock.calls[0][0]
99
+ expect(output).toContain('Hello world')
100
+ })
@@ -0,0 +1,131 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { handleError } from '@/shared/utils/error-handler'
4
+ import { formatOutput } from '@/shared/utils/output'
5
+
6
+ import { TeamsClient } from '../client'
7
+ import { TeamsCredentialManager } from '../credential-manager'
8
+
9
+ export async function listAction(options: { pretty?: boolean }): Promise<void> {
10
+ try {
11
+ const credManager = new TeamsCredentialManager()
12
+ const cred = await credManager.getTokenWithExpiry()
13
+
14
+ if (!cred) {
15
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
16
+ process.exit(1)
17
+ }
18
+
19
+ const client = await new TeamsClient().login({
20
+ token: cred.token,
21
+ tokenExpiresAt: cred.tokenExpiresAt,
22
+ accountType: cred.accountType,
23
+ region: cred.region,
24
+ })
25
+ const chats = await client.listChats()
26
+
27
+ const output = chats.map((chat) => ({
28
+ id: chat.id,
29
+ type: chat.type,
30
+ topic: chat.topic,
31
+ last_message: chat.last_message,
32
+ last_message_at: chat.last_message_at,
33
+ }))
34
+
35
+ console.log(formatOutput(output, options.pretty))
36
+ } catch (error) {
37
+ handleError(error as Error)
38
+ }
39
+ }
40
+
41
+ export async function historyAction(chatId: string, options: { limit?: number; pretty?: boolean }): Promise<void> {
42
+ try {
43
+ const credManager = new TeamsCredentialManager()
44
+ const cred = await credManager.getTokenWithExpiry()
45
+
46
+ if (!cred) {
47
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
48
+ process.exit(1)
49
+ }
50
+
51
+ const client = await new TeamsClient().login({
52
+ token: cred.token,
53
+ tokenExpiresAt: cred.tokenExpiresAt,
54
+ accountType: cred.accountType,
55
+ region: cred.region,
56
+ })
57
+ const limit = options.limit && options.limit > 0 ? options.limit : 50
58
+ const messages = await client.getChatMessages(chatId, limit)
59
+
60
+ const output = messages.map((msg) => ({
61
+ id: msg.id,
62
+ author: msg.author.displayName,
63
+ content: msg.content,
64
+ timestamp: msg.timestamp,
65
+ }))
66
+
67
+ console.log(formatOutput(output, options.pretty))
68
+ } catch (error) {
69
+ handleError(error as Error)
70
+ }
71
+ }
72
+
73
+ export async function sendAction(chatId: string, content: string, options: { pretty?: boolean }): Promise<void> {
74
+ try {
75
+ const credManager = new TeamsCredentialManager()
76
+ const cred = await credManager.getTokenWithExpiry()
77
+
78
+ if (!cred) {
79
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
80
+ process.exit(1)
81
+ }
82
+
83
+ const client = await new TeamsClient().login({
84
+ token: cred.token,
85
+ tokenExpiresAt: cred.tokenExpiresAt,
86
+ accountType: cred.accountType,
87
+ region: cred.region,
88
+ })
89
+ const message = await client.sendChatMessage(chatId, content)
90
+
91
+ const output = {
92
+ id: message.id,
93
+ content: message.content,
94
+ timestamp: message.timestamp,
95
+ }
96
+
97
+ console.log(formatOutput(output, options.pretty))
98
+ } catch (error) {
99
+ handleError(error as Error)
100
+ }
101
+ }
102
+
103
+ export const chatCommand = new Command('chat')
104
+ .description('Chat commands (1:1, group, and self chats)')
105
+ .addCommand(
106
+ new Command('list')
107
+ .description('List 1:1, group, and self chats')
108
+ .option('--pretty', 'Pretty print JSON output')
109
+ .action(listAction),
110
+ )
111
+ .addCommand(
112
+ new Command('history')
113
+ .description('Get chat message history')
114
+ .argument('<chat-id>', 'Chat ID')
115
+ .option('--limit <n>', 'Number of messages to fetch', '50')
116
+ .option('--pretty', 'Pretty print JSON output')
117
+ .action((chatId, options) => {
118
+ return historyAction(chatId, {
119
+ limit: parseInt(options.limit, 10),
120
+ pretty: options.pretty,
121
+ })
122
+ }),
123
+ )
124
+ .addCommand(
125
+ new Command('send')
126
+ .description('Send a message to a chat')
127
+ .argument('<chat-id>', 'Chat ID')
128
+ .argument('<content>', 'Message content')
129
+ .option('--pretty', 'Pretty print JSON output')
130
+ .action(sendAction),
131
+ )
@@ -4,6 +4,7 @@
4
4
 
5
5
  export { authCommand } from './auth'
6
6
  export { channelCommand } from './channel'
7
+ export { chatCommand } from './chat'
7
8
  export { fileCommand } from './file'
8
9
  export { messageCommand } from './message'
9
10
  export { reactionCommand } from './reaction'
@@ -35,6 +35,16 @@ export interface TeamsUser {
35
35
  userPrincipalName?: string
36
36
  }
37
37
 
38
+ export type TeamsChatType = 'oneOnOne' | 'group' | 'self'
39
+
40
+ export interface TeamsChat {
41
+ id: string
42
+ type: TeamsChatType
43
+ topic?: string
44
+ last_message?: string
45
+ last_message_at?: string
46
+ }
47
+
38
48
  export interface TeamsReaction {
39
49
  emoji: string
40
50
  count: number
@@ -124,6 +134,16 @@ export const TeamsUserSchema = z.object({
124
134
  userPrincipalName: z.string().optional(),
125
135
  })
126
136
 
137
+ export const TeamsChatTypeSchema = z.enum(['oneOnOne', 'group', 'self'])
138
+
139
+ export const TeamsChatSchema = z.object({
140
+ id: z.string(),
141
+ type: TeamsChatTypeSchema,
142
+ topic: z.string().optional(),
143
+ last_message: z.string().optional(),
144
+ last_message_at: z.string().optional(),
145
+ })
146
+
127
147
  export const TeamsReactionSchema = z.object({
128
148
  emoji: z.string(),
129
149
  count: z.number(),
@@ -37,7 +37,7 @@ export class LineAdapter implements PlatformAdapter {
37
37
  return messages.map((msg) => ({
38
38
  id: msg.message_id,
39
39
  channelId,
40
- author: msg.author_id ?? 'unknown',
40
+ author: msg.author_name ?? msg.author_id ?? 'unknown',
41
41
  content: msg.text ?? '',
42
42
  timestamp: msg.sent_at,
43
43
  }))