agent-messenger 2.12.2 → 2.13.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 (104) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/discordbot/index.d.ts +2 -1
  4. package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
  5. package/dist/src/platforms/discordbot/index.js.map +1 -1
  6. package/dist/src/platforms/discordbot/listener.d.ts +4 -3
  7. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
  8. package/dist/src/platforms/discordbot/listener.js.map +1 -1
  9. package/dist/src/platforms/discordbot/types.d.ts +21 -0
  10. package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
  11. package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +18 -0
  12. package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -0
  13. package/dist/src/platforms/kakaotalk/chat-classifier.js +29 -0
  14. package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -0
  15. package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
  16. package/dist/src/platforms/kakaotalk/cli.js +2 -1
  17. package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
  18. package/dist/src/platforms/kakaotalk/client.d.ts +13 -1
  19. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  20. package/dist/src/platforms/kakaotalk/client.js +225 -8
  21. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  22. package/dist/src/platforms/kakaotalk/commands/chat.d.ts.map +1 -1
  23. package/dist/src/platforms/kakaotalk/commands/chat.js +2 -1
  24. package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
  25. package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
  26. package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
  27. package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
  28. package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
  29. package/dist/src/platforms/kakaotalk/commands/member.d.ts +3 -0
  30. package/dist/src/platforms/kakaotalk/commands/member.d.ts.map +1 -0
  31. package/dist/src/platforms/kakaotalk/commands/member.js +22 -0
  32. package/dist/src/platforms/kakaotalk/commands/member.js.map +1 -0
  33. package/dist/src/platforms/kakaotalk/index.d.ts +4 -2
  34. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  35. package/dist/src/platforms/kakaotalk/index.js +2 -1
  36. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  37. package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
  38. package/dist/src/platforms/kakaotalk/listener.js +5 -2
  39. package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
  40. package/dist/src/platforms/kakaotalk/protocol/session.d.ts +28 -0
  41. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  42. package/dist/src/platforms/kakaotalk/protocol/session.js +44 -0
  43. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  44. package/dist/src/platforms/kakaotalk/types.d.ts +37 -0
  45. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  46. package/dist/src/platforms/kakaotalk/types.js +17 -0
  47. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  48. package/dist/src/platforms/slackbot/client.d.ts +5 -0
  49. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  50. package/dist/src/platforms/slackbot/client.js +5 -0
  51. package/dist/src/platforms/slackbot/client.js.map +1 -1
  52. package/dist/src/platforms/slackbot/index.d.ts +2 -2
  53. package/dist/src/platforms/slackbot/index.d.ts.map +1 -1
  54. package/dist/src/platforms/slackbot/index.js +1 -1
  55. package/dist/src/platforms/slackbot/index.js.map +1 -1
  56. package/dist/src/tui/adapters/kakaotalk-adapter.js +3 -3
  57. package/dist/src/tui/adapters/kakaotalk-adapter.js.map +1 -1
  58. package/docs/content/docs/cli/kakaotalk.mdx +26 -1
  59. package/docs/content/docs/sdk/kakaotalk.mdx +45 -13
  60. package/docs/content/docs/sdk/slackbot.mdx +11 -0
  61. package/package.json +1 -1
  62. package/scripts/kakao-loco-capture.ts +466 -0
  63. package/skills/agent-channeltalk/SKILL.md +1 -1
  64. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  65. package/skills/agent-discord/SKILL.md +1 -1
  66. package/skills/agent-discordbot/SKILL.md +1 -1
  67. package/skills/agent-instagram/SKILL.md +1 -1
  68. package/skills/agent-kakaotalk/SKILL.md +30 -3
  69. package/skills/agent-kakaotalk/references/common-patterns.md +49 -1
  70. package/skills/agent-line/SKILL.md +1 -1
  71. package/skills/agent-slack/SKILL.md +1 -1
  72. package/skills/agent-slackbot/SKILL.md +1 -2
  73. package/skills/agent-teams/SKILL.md +1 -1
  74. package/skills/agent-telegram/SKILL.md +1 -1
  75. package/skills/agent-telegrambot/SKILL.md +1 -1
  76. package/skills/agent-webex/SKILL.md +1 -1
  77. package/skills/agent-wechatbot/SKILL.md +1 -1
  78. package/skills/agent-whatsapp/SKILL.md +1 -1
  79. package/skills/agent-whatsappbot/SKILL.md +1 -1
  80. package/src/platforms/discordbot/index.test.ts +76 -0
  81. package/src/platforms/discordbot/index.ts +3 -0
  82. package/src/platforms/discordbot/listener.test.ts +41 -0
  83. package/src/platforms/discordbot/listener.ts +5 -1
  84. package/src/platforms/discordbot/types.ts +23 -1
  85. package/src/platforms/kakaotalk/chat-classifier.test.ts +33 -0
  86. package/src/platforms/kakaotalk/chat-classifier.ts +31 -0
  87. package/src/platforms/kakaotalk/cli.ts +2 -1
  88. package/src/platforms/kakaotalk/client.test.ts +782 -1
  89. package/src/platforms/kakaotalk/client.ts +262 -10
  90. package/src/platforms/kakaotalk/commands/chat.ts +3 -1
  91. package/src/platforms/kakaotalk/commands/index.ts +1 -0
  92. package/src/platforms/kakaotalk/commands/member.test.ts +102 -0
  93. package/src/platforms/kakaotalk/commands/member.ts +32 -0
  94. package/src/platforms/kakaotalk/index.test.ts +5 -0
  95. package/src/platforms/kakaotalk/index.ts +4 -0
  96. package/src/platforms/kakaotalk/listener.test.ts +29 -0
  97. package/src/platforms/kakaotalk/listener.ts +5 -2
  98. package/src/platforms/kakaotalk/protocol/session.ts +44 -0
  99. package/src/platforms/kakaotalk/types.ts +39 -0
  100. package/src/platforms/slackbot/client.test.ts +67 -0
  101. package/src/platforms/slackbot/client.ts +17 -1
  102. package/src/platforms/slackbot/index.test.ts +10 -0
  103. package/src/platforms/slackbot/index.ts +4 -0
  104. package/src/tui/adapters/kakaotalk-adapter.ts +3 -3
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-kakaotalk
3
3
  description: Interact with KakaoTalk - send messages, read chats, manage conversations
4
- version: 2.12.2
4
+ version: 2.13.1
5
5
  allowed-tools: Bash(agent-kakaotalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -288,15 +288,39 @@ agent-kakaotalk chat list
288
288
  agent-kakaotalk chat list --pretty
289
289
  agent-kakaotalk chat list --account <account-id>
290
290
  agent-kakaotalk chat list --account <account-id> --pretty
291
+
292
+ # Resolve user-set room titles via CHATINFO (one extra LOCO call per chat;
293
+ # slower, but matches the room name shown in the official KakaoTalk app)
294
+ agent-kakaotalk chat list --resolve-titles
291
295
  ```
292
296
 
293
297
  Output includes:
294
298
  - `chat_id` — numeric chat room ID
295
299
  - `type` — chat type (1:1, group, open chat)
296
300
  - `display_name` — comma-separated member names
301
+ - `title` — user-set room title (only populated with `--resolve-titles`; otherwise `null`). For open chats (`OM` / `OD`) without a user-set title, falls back to the OpenLink room name (one extra `INFOLINK` LOCO call per such chat).
297
302
  - `active_members` — number of active members
298
303
  - `unread_count` — unread message count
299
- - `last_message` — most recent message preview
304
+ - `last_message` — most recent message preview, including `author_name` when the sender's nickname is known from the chat list (otherwise `null`)
305
+
306
+ ### Member Commands
307
+
308
+ ```bash
309
+ # List all members of a chat room (uses LOCO GETMEM — one call per invocation)
310
+ agent-kakaotalk member list <chat-id>
311
+ agent-kakaotalk member list <chat-id> --pretty
312
+ agent-kakaotalk member list <chat-id> --account <account-id>
313
+ ```
314
+
315
+ Each member includes:
316
+ - `user_id` — numeric user ID (string for safety)
317
+ - `nickname` — display name in this chat (open chats may differ from the user's main Kakao nickname)
318
+ - `profile_image_url`, `full_profile_image_url`, `original_profile_image_url`
319
+ - `status_message`, `country_iso`
320
+ - `user_type` — KakaoTalk's user type (100 = friend, 1000 = open profile, etc.); `null` when the server omits the field
321
+ - `open_token`, `open_profile_link_id`, `open_permission` — open-chat-only fields (`null` for normal chats; `open_permission` is 1=OWNER, 2=NONE, 4=MANAGER, 8=BOT)
322
+
323
+ > **SDK-only**: `KakaoTalkClient.getMembersByIds(chatId, userIds)` is available for the >100-member case where you already have specific user IDs to resolve (typically from a CHATONROOM `mi` array). It is intentionally not exposed via the CLI because acquiring those IDs requires a CHATONROOM call that is also SDK-only. Use `agent-kakaotalk member list` for the common case.
300
324
 
301
325
  ### Message Commands
302
326
 
@@ -322,6 +346,7 @@ Each message includes:
322
346
  - `log_id` — unique message identifier
323
347
  - `type` — message type (1 = text, 2 = photo, etc.)
324
348
  - `author_id` — sender's user ID
349
+ - `author_name` — sender's nickname when known from the chat list (otherwise `null`; only the room's "display members" are cached)
325
350
  - `message` — message text content
326
351
  - `sent_at` — Unix timestamp (milliseconds)
327
352
 
@@ -354,10 +379,12 @@ All commands output JSON by default for AI consumption:
354
379
  "chat_id": "9876543210",
355
380
  "type": 2,
356
381
  "display_name": "Alice, Bob",
382
+ "title": null,
357
383
  "active_members": 3,
358
384
  "unread_count": 5,
359
385
  "last_message": {
360
- "author_id": "1111111111",
386
+ "author_id": 1111111111,
387
+ "author_name": "Alice",
361
388
  "message": "Hello everyone!",
362
389
  "sent_at": 1705312200000
363
390
  }
@@ -55,6 +55,8 @@ agent-kakaotalk message send "$TARGET_CHAT" "Hey Alice!"
55
55
 
56
56
  **When to use**: First time interacting with a chat, or when the user references a chat by name.
57
57
 
58
+ > Note: `display_name` joins the chat's member nicknames. For the user-set room title (matching the KakaoTalk app), see [Pattern 9](#pattern-9-resolve-canonical-room-titles).
59
+
58
60
  ## Pattern 3: Monitor Chat for New Messages
59
61
 
60
62
  **Use case**: Watch a chat room and respond to new messages
@@ -221,7 +223,53 @@ echo "$UNREAD" | jq -r '.[] | " \(.display_name // "Unknown") — \(.unread_cou
221
223
 
222
224
  **When to use**: Morning catch-up, checking for urgent messages, triage.
223
225
 
224
- ## Pattern 9: Error Handling and Retry
226
+ ## Pattern 9: Resolve Canonical Room Titles
227
+
228
+ **Use case**: Show user-set room names (matching the official KakaoTalk app) instead of comma-joined member nicknames
229
+
230
+ By default, `chat list` returns `display_name` built from the chat's "display members" (a comma-joined nickname list — e.g. `"Alice, Bob, Charlie"`). The `--resolve-titles` flag fetches each chat's user-set title via the LOCO `CHATINFO` command and surfaces it in a separate `title` field.
231
+
232
+ For open chats (`OM` / `OD`) without a user-set title, the CLI additionally consults the OpenLink record via `INFOLINK` and uses the link name as a fallback. This matches what KakaoTalk shows for open-chat rooms in the app sidebar.
233
+
234
+ ```bash
235
+ #!/bin/bash
236
+
237
+ # Without --resolve-titles: title is null, display_name is member nicknames
238
+ agent-kakaotalk chat list | jq '.[0] | {chat_id, display_name, title}'
239
+ # {
240
+ # "chat_id": "9876543210",
241
+ # "display_name": "Alice, Bob, Charlie",
242
+ # "title": null
243
+ # }
244
+
245
+ # With --resolve-titles: title is the user-set room name
246
+ agent-kakaotalk chat list --resolve-titles | jq '.[0] | {chat_id, display_name, title}'
247
+ # {
248
+ # "chat_id": "9876543210",
249
+ # "display_name": "Alice, Bob, Charlie",
250
+ # "title": "Project Standup"
251
+ # }
252
+
253
+ # Render the best available name (title preferred, display_name fallback)
254
+ agent-kakaotalk chat list --resolve-titles \
255
+ | jq -r '.[] | "\(.chat_id): \(.title // .display_name // "Untitled")"'
256
+ ```
257
+
258
+ **When to use**: User-facing chat pickers, room-name displays in summaries, anywhere you want output that matches what the user sees in the KakaoTalk app.
259
+
260
+ **Cost**: One extra LOCO call per chat (CHATINFO). Open chats without a user-set title pay one additional call (INFOLINK). For large account snapshots this multiplies quickly — leave the flag off for hot paths.
261
+
262
+ **SDK equivalent**:
263
+
264
+ ```typescript
265
+ // Resolve titles for the whole list
266
+ const chats = await client.getChats({ resolveTitles: true })
267
+
268
+ // Single-chat lookup (returns null on error or missing title)
269
+ const title = await client.getChatTitle('9876543210')
270
+ ```
271
+
272
+ ## Pattern 10: Error Handling and Retry
225
273
 
226
274
  **Use case**: Robust message sending with retries
227
275
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-line
3
3
  description: Interact with LINE - send messages, read chats, manage conversations
4
- version: 2.12.2
4
+ version: 2.13.1
5
5
  allowed-tools: Bash(agent-line:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.12.2
4
+ version: 2.13.1
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.12.2
4
+ version: 2.13.1
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -365,7 +365,6 @@ Credentials stored in `~/.config/agent-messenger/slackbot-credentials.json` (060
365
365
  - Bot can only edit/delete its own messages
366
366
  - Bot must be invited to private channels
367
367
  - No scheduled messages
368
- - Plain text messages only (no blocks/formatting)
369
368
 
370
369
  ## Troubleshooting
371
370
 
@@ -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.12.2
4
+ version: 2.13.1
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -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.12.2
4
+ version: 2.13.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.12.2
4
+ version: 2.13.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.12.2
4
+ version: 2.13.1
5
5
  allowed-tools: Bash(agent-webex:*)
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.12.2
4
+ version: 2.13.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.12.2
4
+ version: 2.13.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.12.2
4
+ version: 2.13.1
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -17,6 +17,12 @@ import {
17
17
  DiscordReactionSchema,
18
18
  DiscordUserSchema,
19
19
  } from '@/platforms/discordbot/index'
20
+ import type {
21
+ DiscordBotListenerOptions,
22
+ DiscordGatewayEmbed,
23
+ DiscordGatewayMessageCreateEvent,
24
+ DiscordGatewayStickerItem,
25
+ } from '@/platforms/discordbot/index'
20
26
 
21
27
  it('DiscordBotClient is exported from barrel', () => {
22
28
  expect(typeof DiscordBotClient).toBe('function')
@@ -34,6 +40,11 @@ it('DiscordBotListener is exported from barrel', () => {
34
40
  expect(typeof DiscordBotListener).toBe('function')
35
41
  })
36
42
 
43
+ it('DiscordBotListenerOptions type is exported from barrel', () => {
44
+ const options: DiscordBotListenerOptions = { intents: 0 }
45
+ expect(options.intents).toBe(0)
46
+ })
47
+
37
48
  it('DiscordGatewayOpcode is exported from barrel', () => {
38
49
  expect(DiscordGatewayOpcode.Identify).toBe(2)
39
50
  expect(DiscordGatewayOpcode.Hello).toBe(10)
@@ -80,3 +91,68 @@ it('DiscordReactionSchema is exported from barrel', () => {
80
91
  it('DiscordFileSchema is exported from barrel', () => {
81
92
  expect(typeof DiscordFileSchema.parse).toBe('function')
82
93
  })
94
+
95
+ it('DiscordGatewayEmbed type is exported from barrel', () => {
96
+ const embed: DiscordGatewayEmbed = {
97
+ type: 'rich',
98
+ title: 'Release notes',
99
+ description: 'v2.13.1 is out',
100
+ url: 'https://example.com/release',
101
+ }
102
+ expect(embed.title).toBe('Release notes')
103
+ })
104
+
105
+ it('DiscordGatewayStickerItem type is exported from barrel', () => {
106
+ const sticker: DiscordGatewayStickerItem = { id: '1', name: 'wave', format_type: 1 }
107
+ expect(sticker.format_type).toBe(1)
108
+ })
109
+
110
+ it('DiscordGatewayMessageCreateEvent accepts the new optional fields', () => {
111
+ const event: DiscordGatewayMessageCreateEvent = {
112
+ type: 'MESSAGE_CREATE',
113
+ id: 'm1',
114
+ channel_id: 'C1',
115
+ author: { id: 'U1', username: 'alice', global_name: 'Alice', bot: false },
116
+ content: 'hello',
117
+ timestamp: '2026-05-11T00:00:00Z',
118
+ mention_everyone: false,
119
+ mention_roles: ['R1', 'R2'],
120
+ message_reference: { message_id: 'm0', channel_id: 'C1', guild_id: 'G1' },
121
+ embeds: [{ type: 'rich', title: 't', description: 'd', url: 'https://example.com' }],
122
+ sticker_items: [{ id: 's1', name: 'wave', format_type: 1 }],
123
+ }
124
+ expect(event.mention_everyone).toBe(false)
125
+ expect(event.mention_roles).toEqual(['R1', 'R2'])
126
+ expect(event.message_reference?.message_id).toBe('m0')
127
+ expect(event.embeds?.[0]?.title).toBe('t')
128
+ expect(event.sticker_items?.[0]?.name).toBe('wave')
129
+ expect(event.author.global_name).toBe('Alice')
130
+ })
131
+
132
+ it('DiscordGatewayMessageCreateEvent treats new fields as optional', () => {
133
+ const event: DiscordGatewayMessageCreateEvent = {
134
+ type: 'MESSAGE_CREATE',
135
+ id: 'm1',
136
+ channel_id: 'C1',
137
+ author: { id: 'U1', username: 'alice' },
138
+ content: 'hello',
139
+ timestamp: '2026-05-11T00:00:00Z',
140
+ }
141
+ expect(event.mention_everyone).toBeUndefined()
142
+ expect(event.embeds).toBeUndefined()
143
+ expect(event.sticker_items).toBeUndefined()
144
+ expect(event.message_reference).toBeUndefined()
145
+ expect(event.author.global_name).toBeUndefined()
146
+ })
147
+
148
+ it('DiscordGatewayMessageCreateEvent author.global_name accepts null', () => {
149
+ const event: DiscordGatewayMessageCreateEvent = {
150
+ type: 'MESSAGE_CREATE',
151
+ id: 'm1',
152
+ channel_id: 'C1',
153
+ author: { id: 'U1', username: 'alice', global_name: null },
154
+ content: 'hello',
155
+ timestamp: '2026-05-11T00:00:00Z',
156
+ }
157
+ expect(event.author.global_name).toBeNull()
158
+ })
@@ -1,6 +1,7 @@
1
1
  export { DiscordBotClient } from './client'
2
2
  export { DiscordBotCredentialManager } from './credential-manager'
3
3
  export { DiscordBotListener } from './listener'
4
+ export type { DiscordBotListenerOptions } from './listener'
4
5
  export type {
5
6
  DiscordBotConfig,
6
7
  DiscordBotCredentials,
@@ -9,6 +10,7 @@ export type {
9
10
  DiscordChannel,
10
11
  DiscordFile,
11
12
  DiscordGatewayChannelEvent,
13
+ DiscordGatewayEmbed,
12
14
  DiscordGatewayEvent,
13
15
  DiscordGatewayGenericEvent,
14
16
  DiscordGatewayGuildEvent,
@@ -19,6 +21,7 @@ export type {
19
21
  DiscordGatewayMessageUpdateEvent,
20
22
  DiscordGatewayPresenceEvent,
21
23
  DiscordGatewayReactionEvent,
24
+ DiscordGatewayStickerItem,
22
25
  DiscordGatewayTypingEvent,
23
26
  DiscordGuild,
24
27
  DiscordMessage,
@@ -235,6 +235,47 @@ describe('DiscordBotListener', () => {
235
235
  expect(messages[0].channel_id).toBe('C123')
236
236
  })
237
237
 
238
+ it('surfaces global_name, mentions, message_reference, embeds, and sticker_items', async () => {
239
+ const client = createMockClient()
240
+ listener = new DiscordBotListener(client)
241
+
242
+ const messages: DiscordGatewayMessageCreateEvent[] = []
243
+ listener.on('message_create', (event) => messages.push(event))
244
+
245
+ await listener.start()
246
+ mockWsInstance.simulateOpen()
247
+ mockWsInstance.simulateHello()
248
+ mockWsInstance.simulateReady()
249
+ mockWsInstance.simulateDispatch(
250
+ 'MESSAGE_CREATE',
251
+ {
252
+ id: 'msg_2',
253
+ channel_id: 'C123',
254
+ author: { id: 'U456', username: 'user', global_name: 'User Display' },
255
+ content: 'reply text',
256
+ timestamp: '2024-01-01T00:00:00Z',
257
+ mention_everyone: false,
258
+ mention_roles: ['R1'],
259
+ message_reference: { message_id: 'msg_orig', channel_id: 'C123', guild_id: 'G1' },
260
+ embeds: [{ type: 'rich', title: 'embed-title', description: 'embed-desc', url: 'https://example.com' }],
261
+ sticker_items: [{ id: 'sk1', name: 'wave', format_type: 1 }],
262
+ },
263
+ 2,
264
+ )
265
+
266
+ expect(messages.length).toBe(1)
267
+ const event = messages[0]
268
+ expect(event.author.global_name).toBe('User Display')
269
+ expect(event.mention_everyone).toBe(false)
270
+ expect(event.mention_roles).toEqual(['R1'])
271
+ expect(event.message_reference?.message_id).toBe('msg_orig')
272
+ expect(event.message_reference?.guild_id).toBe('G1')
273
+ expect(event.embeds?.[0]?.title).toBe('embed-title')
274
+ expect(event.embeds?.[0]?.url).toBe('https://example.com')
275
+ expect(event.sticker_items?.[0]?.name).toBe('wave')
276
+ expect(event.sticker_items?.[0]?.format_type).toBe(1)
277
+ })
278
+
238
279
  it('emits message_update events', async () => {
239
280
  const client = createMockClient()
240
281
  listener = new DiscordBotListener(client)
@@ -24,6 +24,10 @@ const DEFAULT_INTENTS =
24
24
 
25
25
  type EventKey = keyof DiscordBotListenerEventMap
26
26
 
27
+ export interface DiscordBotListenerOptions {
28
+ intents?: number
29
+ }
30
+
27
31
  export class DiscordBotListener {
28
32
  private client: DiscordBotClient
29
33
  private intents: number
@@ -43,7 +47,7 @@ export class DiscordBotListener {
43
47
  private cachedUser: { id: string; username: string } | null = null
44
48
  private generation = 0
45
49
 
46
- constructor(client: DiscordBotClient, options?: { intents?: number }) {
50
+ constructor(client: DiscordBotClient, options?: DiscordBotListenerOptions) {
47
51
  this.client = client
48
52
  this.intents = options?.intents ?? DEFAULT_INTENTS
49
53
  }
@@ -224,17 +224,39 @@ export const DiscordIntent = {
224
224
  AutoModerationExecution: 1 << 21,
225
225
  } as const
226
226
 
227
+ export interface DiscordGatewayEmbed {
228
+ type?: string
229
+ title?: string
230
+ description?: string
231
+ url?: string
232
+ }
233
+
234
+ export interface DiscordGatewayStickerItem {
235
+ id: string
236
+ name: string
237
+ format_type?: number
238
+ }
239
+
227
240
  export interface DiscordGatewayMessageCreateEvent {
228
241
  type: 'MESSAGE_CREATE'
229
242
  id: string
230
243
  channel_id: string
231
244
  guild_id?: string
232
- author: { id: string; username: string; bot?: boolean }
245
+ author: { id: string; username: string; global_name?: string | null; bot?: boolean }
233
246
  content: string
234
247
  timestamp: string
235
248
  edited_timestamp?: string
236
249
  mentions?: DiscordUser[]
250
+ mention_everyone?: boolean
251
+ mention_roles?: string[]
252
+ message_reference?: {
253
+ message_id?: string
254
+ channel_id?: string
255
+ guild_id?: string
256
+ }
237
257
  attachments?: DiscordFile[]
258
+ embeds?: DiscordGatewayEmbed[]
259
+ sticker_items?: DiscordGatewayStickerItem[]
238
260
  }
239
261
 
240
262
  export interface DiscordGatewayMessageUpdateEvent {
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { classifyKakaoChat } from './chat-classifier'
4
+
5
+ describe('classifyKakaoChat', () => {
6
+ it('classifies a normal DM (type 11, 2 members) as dm', () => {
7
+ expect(classifyKakaoChat({ type: 11, active_members: 2 })).toBe('dm')
8
+ })
9
+
10
+ it('classifies a normal group (type 10, 5 members) as group', () => {
11
+ expect(classifyKakaoChat({ type: 10, active_members: 5 })).toBe('group')
12
+ })
13
+
14
+ it('classifies legacy DM (type 9, 2 members) as dm via member-count fallback', () => {
15
+ expect(classifyKakaoChat({ type: 9, active_members: 2 })).toBe('dm')
16
+ })
17
+
18
+ it('classifies a 3-member group (type 10) as group, not dm', () => {
19
+ expect(classifyKakaoChat({ type: 10, active_members: 3 })).toBe('group')
20
+ })
21
+
22
+ for (const type of [2, 13, 14, 15, 16]) {
23
+ it(`classifies open-chat type ${type} as open regardless of member count`, () => {
24
+ expect(classifyKakaoChat({ type, active_members: 1 })).toBe('open')
25
+ expect(classifyKakaoChat({ type, active_members: 2 })).toBe('open')
26
+ expect(classifyKakaoChat({ type, active_members: 50 })).toBe('open')
27
+ })
28
+ }
29
+
30
+ it('classifies a 1-member chat (lone room) as dm', () => {
31
+ expect(classifyKakaoChat({ type: 11, active_members: 1 })).toBe('dm')
32
+ })
33
+ })
@@ -0,0 +1,31 @@
1
+ import type { KakaoChat } from './types'
2
+
3
+ export type KakaoChatKind = 'dm' | 'group' | 'open' | 'unknown'
4
+
5
+ // OpenChat-family `type` codes observed on the wire. KakaoTalk's LOCO
6
+ // protocol exposes a numeric `type` field with no documented mapping; these
7
+ // five codes consistently identify OpenChat rooms across normal OpenChat,
8
+ // OpenChat DMs, and the various OpenChat sub-types seen in production.
9
+ const OPEN_CHAT_TYPE_CODES: ReadonlySet<number> = new Set([2, 13, 14, 15, 16])
10
+
11
+ /**
12
+ * Classify a KakaoTalk chat as `'dm'`, `'group'`, `'open'`, or `'unknown'`.
13
+ *
14
+ * REGRESSION GUARD: An earlier implementation hard-coded `0=dm`, `1=group`,
15
+ * `2=open` on the raw `type` number. Modern KakaoTalk uses codes like `11`
16
+ * for normal DMs and `10` for normal groups, so the old mapping silently
17
+ * classified every real DM as `'unknown'` and bucketed it as a group. Do
18
+ * NOT "simplify" this back to a pure type-code mapping without verifying
19
+ * against a real KakaoTalk session.
20
+ *
21
+ * `'unknown'` is reserved for future protocol drift; the current heuristic
22
+ * never returns it, but it is part of the union so consumers can handle
23
+ * the case defensively.
24
+ */
25
+ export function classifyKakaoChat(chat: Pick<KakaoChat, 'type' | 'active_members'>): KakaoChatKind {
26
+ if (OPEN_CHAT_TYPE_CODES.has(chat.type)) return 'open'
27
+ // active_members counts the logged-in user, so a 1:1 DM is exactly 2
28
+ // (self + one other) and a "lone" room with only self is 1.
29
+ if (chat.active_members <= 2) return 'dm'
30
+ return 'group'
31
+ }
@@ -4,7 +4,7 @@ import type { Command as CommandType } from 'commander'
4
4
  import { Command } from 'commander'
5
5
 
6
6
  import pkg from '../../../package.json' with { type: 'json' }
7
- import { authCommand, chatCommand, messageCommand, whoamiCommand } from './commands/index'
7
+ import { authCommand, chatCommand, memberCommand, messageCommand, whoamiCommand } from './commands/index'
8
8
  import { ensureKakaoAuth } from './ensure-auth'
9
9
 
10
10
  function isAuthCommand(command: CommandType): boolean {
@@ -30,6 +30,7 @@ program.hook('preAction', async (_thisCommand, actionCommand) => {
30
30
 
31
31
  program.addCommand(authCommand)
32
32
  program.addCommand(chatCommand)
33
+ program.addCommand(memberCommand)
33
34
  program.addCommand(messageCommand)
34
35
  program.addCommand(whoamiCommand)
35
36