agent-messenger 2.12.1 → 2.13.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 (87) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +18 -0
  4. package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -0
  5. package/dist/src/platforms/kakaotalk/chat-classifier.js +29 -0
  6. package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -0
  7. package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
  8. package/dist/src/platforms/kakaotalk/cli.js +2 -1
  9. package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
  10. package/dist/src/platforms/kakaotalk/client.d.ts +35 -1
  11. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  12. package/dist/src/platforms/kakaotalk/client.js +318 -15
  13. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  14. package/dist/src/platforms/kakaotalk/commands/chat.d.ts.map +1 -1
  15. package/dist/src/platforms/kakaotalk/commands/chat.js +2 -1
  16. package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
  17. package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
  18. package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
  19. package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
  20. package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
  21. package/dist/src/platforms/kakaotalk/commands/member.d.ts +3 -0
  22. package/dist/src/platforms/kakaotalk/commands/member.d.ts.map +1 -0
  23. package/dist/src/platforms/kakaotalk/commands/member.js +22 -0
  24. package/dist/src/platforms/kakaotalk/commands/member.js.map +1 -0
  25. package/dist/src/platforms/kakaotalk/index.d.ts +4 -2
  26. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  27. package/dist/src/platforms/kakaotalk/index.js +2 -1
  28. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  29. package/dist/src/platforms/kakaotalk/listener.d.ts +4 -7
  30. package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
  31. package/dist/src/platforms/kakaotalk/listener.js +48 -74
  32. package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
  33. package/dist/src/platforms/kakaotalk/protocol/session.d.ts +28 -0
  34. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  35. package/dist/src/platforms/kakaotalk/protocol/session.js +44 -0
  36. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  37. package/dist/src/platforms/kakaotalk/types.d.ts +37 -0
  38. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  39. package/dist/src/platforms/kakaotalk/types.js +17 -0
  40. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  41. package/dist/src/platforms/slackbot/client.d.ts +5 -0
  42. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  43. package/dist/src/platforms/slackbot/client.js +5 -0
  44. package/dist/src/platforms/slackbot/client.js.map +1 -1
  45. package/dist/src/tui/adapters/kakaotalk-adapter.js +3 -3
  46. package/dist/src/tui/adapters/kakaotalk-adapter.js.map +1 -1
  47. package/docs/content/docs/cli/kakaotalk.mdx +26 -1
  48. package/docs/content/docs/sdk/kakaotalk.mdx +45 -13
  49. package/docs/content/docs/sdk/slackbot.mdx +11 -0
  50. package/package.json +1 -1
  51. package/scripts/kakao-loco-capture.ts +466 -0
  52. package/skills/agent-channeltalk/SKILL.md +1 -1
  53. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  54. package/skills/agent-discord/SKILL.md +1 -1
  55. package/skills/agent-discordbot/SKILL.md +1 -1
  56. package/skills/agent-instagram/SKILL.md +1 -1
  57. package/skills/agent-kakaotalk/SKILL.md +30 -3
  58. package/skills/agent-kakaotalk/references/common-patterns.md +49 -1
  59. package/skills/agent-line/SKILL.md +1 -1
  60. package/skills/agent-slack/SKILL.md +1 -1
  61. package/skills/agent-slackbot/SKILL.md +1 -2
  62. package/skills/agent-teams/SKILL.md +1 -1
  63. package/skills/agent-telegram/SKILL.md +1 -1
  64. package/skills/agent-telegrambot/SKILL.md +1 -1
  65. package/skills/agent-webex/SKILL.md +1 -1
  66. package/skills/agent-wechatbot/SKILL.md +1 -1
  67. package/skills/agent-whatsapp/SKILL.md +1 -1
  68. package/skills/agent-whatsappbot/SKILL.md +1 -1
  69. package/src/platforms/kakaotalk/chat-classifier.test.ts +33 -0
  70. package/src/platforms/kakaotalk/chat-classifier.ts +31 -0
  71. package/src/platforms/kakaotalk/cli.ts +2 -1
  72. package/src/platforms/kakaotalk/client-listener-integration.test.ts +411 -0
  73. package/src/platforms/kakaotalk/client.test.ts +785 -1
  74. package/src/platforms/kakaotalk/client.ts +369 -18
  75. package/src/platforms/kakaotalk/commands/chat.ts +3 -1
  76. package/src/platforms/kakaotalk/commands/index.ts +1 -0
  77. package/src/platforms/kakaotalk/commands/member.test.ts +102 -0
  78. package/src/platforms/kakaotalk/commands/member.ts +32 -0
  79. package/src/platforms/kakaotalk/index.test.ts +5 -0
  80. package/src/platforms/kakaotalk/index.ts +4 -0
  81. package/src/platforms/kakaotalk/listener.test.ts +184 -149
  82. package/src/platforms/kakaotalk/listener.ts +51 -82
  83. package/src/platforms/kakaotalk/protocol/session.ts +44 -0
  84. package/src/platforms/kakaotalk/types.ts +39 -0
  85. package/src/platforms/slackbot/client.test.ts +67 -0
  86. package/src/platforms/slackbot/client.ts +17 -1
  87. package/src/tui/adapters/kakaotalk-adapter.ts +3 -3
@@ -1,7 +1,6 @@
1
1
  import { EventEmitter } from 'events'
2
2
 
3
- import type { KakaoTalkClient } from './client'
4
- import { LocoSession } from './protocol/session'
3
+ import type { KakaoSessionEvent, KakaoTalkClient } from './client'
5
4
  import type { LocoPacket } from './protocol/types'
6
5
  import type {
7
6
  KakaoTalkListenerEventMap,
@@ -11,9 +10,6 @@ import type {
11
10
  KakaoTalkPushReadEvent,
12
11
  } from './types'
13
12
 
14
- const RECONNECT_BASE_DELAY = 1_000
15
- const RECONNECT_MAX_DELAY = 30_000
16
-
17
13
  type EventKey = keyof KakaoTalkListenerEventMap
18
14
 
19
15
  function longToString(v: unknown): string {
@@ -27,11 +23,9 @@ function longToString(v: unknown): string {
27
23
  export class KakaoTalkListener {
28
24
  private client: KakaoTalkClient
29
25
  private running = false
30
- private session: LocoSession | null = null
31
26
  private emitter = new EventEmitter()
32
- private reconnectTimer: ReturnType<typeof setTimeout> | null = null
33
- private reconnectAttempts = 0
34
- private userId: string | null = null
27
+ private unsubscribePush: (() => void) | null = null
28
+ private unsubscribeSession: (() => void) | null = null
35
29
 
36
30
  constructor(client: KakaoTalkClient) {
37
31
  this.client = client
@@ -40,17 +34,33 @@ export class KakaoTalkListener {
40
34
  async start(): Promise<void> {
41
35
  if (this.running) return
42
36
  this.running = true
43
- this.reconnectAttempts = 0
44
- await this.connect()
37
+
38
+ this.unsubscribePush = this.client.onPush((packet) => this.handlePush(packet))
39
+ this.unsubscribeSession = this.client.onSessionEvent((event) => this.handleSessionEvent(event))
40
+
41
+ const alreadyConnected = this.client.isConnected()
42
+
43
+ try {
44
+ await this.client.acquireSession()
45
+ if (!this.running) return
46
+ if (alreadyConnected) {
47
+ const { userId } = this.client.getCredentials()
48
+ this.emitter.emit('connected', { userId })
49
+ }
50
+ } catch (error) {
51
+ this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
52
+ this.running = false
53
+ this.teardown()
54
+ }
45
55
  }
46
56
 
47
57
  stop(): void {
48
- this.running = false
49
- this.clearTimers()
50
- if (this.session) {
51
- this.session.close()
52
- this.session = null
58
+ if (!this.running) {
59
+ this.teardown()
60
+ return
53
61
  }
62
+ this.running = false
63
+ this.teardown()
54
64
  }
55
65
 
56
66
  on<K extends EventKey>(event: K, listener: (...args: KakaoTalkListenerEventMap[K]) => void): this {
@@ -68,41 +78,28 @@ export class KakaoTalkListener {
68
78
  return this
69
79
  }
70
80
 
71
- private async connect(): Promise<void> {
72
- if (!this.running) return
73
-
74
- try {
75
- const { oauthToken, userId, deviceUuid, deviceType } = this.client.getCredentials()
76
- if (!this.running) return
77
-
78
- this.userId = userId
79
- const session = new LocoSession()
80
-
81
- session.onPush((packet) => this.handlePush(packet))
82
- session.onClose(() => {
83
- if (this.session !== session) return
84
- this.session = null
85
- if (this.running) {
86
- this.emitter.emit('disconnected')
87
- this.scheduleReconnect()
88
- }
89
- })
90
-
91
- await session.login(oauthToken, userId, deviceUuid, undefined, deviceType)
81
+ private teardown(): void {
82
+ this.unsubscribePush?.()
83
+ this.unsubscribePush = null
84
+ this.unsubscribeSession?.()
85
+ this.unsubscribeSession = null
86
+ }
92
87
 
93
- if (!this.running) {
94
- session.close()
95
- return
96
- }
88
+ private handleSessionEvent(event: KakaoSessionEvent): void {
89
+ if (!this.running) return
97
90
 
98
- this.reconnectAttempts = 0
99
- this.session = session
100
- this.emitter.emit('connected', { userId })
101
- } catch (error) {
102
- this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
103
- if (this.running) {
104
- this.scheduleReconnect()
105
- }
91
+ switch (event.type) {
92
+ case 'connected':
93
+ this.emitter.emit('connected', { userId: event.userId })
94
+ break
95
+ case 'disconnected':
96
+ this.emitter.emit('disconnected')
97
+ break
98
+ case 'kicked':
99
+ this.emitter.emit('error', new Error(event.reason))
100
+ this.running = false
101
+ this.teardown()
102
+ break
106
103
  }
107
104
  }
108
105
 
@@ -112,11 +109,14 @@ export class KakaoTalkListener {
112
109
  switch (method) {
113
110
  case 'MSG': {
114
111
  const chatLog = body.chatLog as Record<string, unknown>
112
+ const chatId = longToString(body.chatId)
113
+ const authorId = chatLog.authorId as number
115
114
  const event: KakaoTalkPushMessageEvent = {
116
115
  type: 'MSG',
117
- chat_id: longToString(body.chatId),
116
+ chat_id: chatId,
118
117
  log_id: longToString(chatLog.logId),
119
- author_id: chatLog.authorId as number,
118
+ author_id: authorId,
119
+ author_name: this.client.lookupAuthorName?.(chatId, authorId) ?? null,
120
120
  message: chatLog.message as string,
121
121
  message_type: chatLog.type as number,
122
122
  sent_at: chatLog.sendAt as number,
@@ -162,23 +162,6 @@ export class KakaoTalkListener {
162
162
  break
163
163
  }
164
164
 
165
- case 'CHANGESVR': {
166
- this.reconnectAttempts = 0
167
- const prev = this.session
168
- this.session = null
169
- prev?.close()
170
- this.connect()
171
- break
172
- }
173
-
174
- case 'KICKOUT': {
175
- this.emitter.emit('error', new Error('Session kicked — another device logged in'))
176
- this.running = false
177
- this.session?.close()
178
- this.session = null
179
- break
180
- }
181
-
182
165
  default: {
183
166
  const event: KakaoTalkPushGenericEvent = { type: method, ...body }
184
167
  this.emitter.emit('kakaotalk_event', event)
@@ -186,18 +169,4 @@ export class KakaoTalkListener {
186
169
  }
187
170
  }
188
171
  }
189
-
190
- private scheduleReconnect(): void {
191
- this.clearTimers()
192
- const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** this.reconnectAttempts, RECONNECT_MAX_DELAY)
193
- this.reconnectAttempts++
194
- this.reconnectTimer = setTimeout(() => this.connect(), delay)
195
- }
196
-
197
- private clearTimers(): void {
198
- if (this.reconnectTimer) {
199
- clearTimeout(this.reconnectTimer)
200
- this.reconnectTimer = null
201
- }
202
- }
203
172
  }
@@ -149,6 +149,50 @@ export class LocoSession {
149
149
  return this.connection.sendPacket('CHATONROOM', { chatId })
150
150
  }
151
151
 
152
+ /**
153
+ * Fetch detailed channel info (CHATINFO). Unlike CHATONROOM, this returns
154
+ * a `chatInfo` sub-document containing `chatMetas` (room title, notice, etc.)
155
+ * and `displayMembers` (user_id ↔ nickname pairs). Used to resolve the
156
+ * canonical user-set room title.
157
+ */
158
+ async getChannelInfo(chatId: Long): Promise<LocoPacket> {
159
+ if (!this.connection) throw new Error('Not connected')
160
+ return this.connection.sendPacket('CHATINFO', { chatId })
161
+ }
162
+
163
+ /**
164
+ * Fetch open-link info (INFOLINK) for one or more openchat links. The
165
+ * response body has shape `{ ols: OpenLinkStruct[] }` where each struct's
166
+ * `ln` field carries the open-chat link name — the canonical fallback when
167
+ * an open chat has no user-set TITLE meta.
168
+ */
169
+ async getOpenLinkInfo(linkIds: Long[]): Promise<LocoPacket> {
170
+ if (!this.connection) throw new Error('Not connected')
171
+ return this.connection.sendPacket('INFOLINK', { lis: linkIds })
172
+ }
173
+
174
+ /**
175
+ * Fetch the full member list for a chat (GETMEM). Response body has shape
176
+ * `{ members: NormalMemberStruct[] | OpenMemberStruct[], token: number }`.
177
+ * Use this to resolve nicknames for users not present in the chat list's
178
+ * "display members" cache — necessary for groups with more than ~5 members.
179
+ */
180
+ async getAllMembers(chatId: Long): Promise<LocoPacket> {
181
+ if (!this.connection) throw new Error('Not connected')
182
+ return this.connection.sendPacket('GETMEM', { chatId })
183
+ }
184
+
185
+ /**
186
+ * Fetch info for a specific subset of members in a chat (MEMBER). Response
187
+ * body has shape `{ chatId, members: NormalMemberStruct[] | OpenMemberStruct[] }`.
188
+ * Useful when you already have user IDs (e.g. from a CHATONROOM `mi` array
189
+ * for >100-member rooms) and only need to resolve a few of them.
190
+ */
191
+ async getMembersByIds(chatId: Long, memberIds: Long[]): Promise<LocoPacket> {
192
+ if (!this.connection) throw new Error('Not connected')
193
+ return this.connection.sendPacket('MEMBER', { chatId, memberIds })
194
+ }
195
+
152
196
  async getChatList(lastTokenId?: Long, lastChatId?: Long): Promise<LocoPacket> {
153
197
  if (!this.connection) throw new Error('Not connected')
154
198
  return this.connection.sendPacket('LCHATLIST', {
@@ -73,10 +73,12 @@ export interface KakaoChat {
73
73
  chat_id: string
74
74
  type: number
75
75
  display_name: string | null
76
+ title: string | null
76
77
  active_members: number
77
78
  unread_count: number
78
79
  last_message: {
79
80
  author_id: number
81
+ author_name: string | null
80
82
  message: string
81
83
  sent_at: number
82
84
  } | null
@@ -86,10 +88,28 @@ export interface KakaoMessage {
86
88
  log_id: string
87
89
  type: number
88
90
  author_id: number
91
+ author_name: string | null
89
92
  message: string
90
93
  sent_at: number
91
94
  }
92
95
 
96
+ export interface KakaoMember {
97
+ user_id: string
98
+ nickname: string
99
+ profile_image_url: string | null
100
+ full_profile_image_url: string | null
101
+ original_profile_image_url: string | null
102
+ status_message: string | null
103
+ country_iso: string | null
104
+ /** KakaoTalk UserType: 100=FRIEND, 1000=OPEN_PROFILE, etc. `null` when the server omits the field. */
105
+ user_type: number | null
106
+ /** Open-chat-only fields below; `null` for normal chats. */
107
+ open_token: number | null
108
+ open_profile_link_id: string | null
109
+ /** OpenChannelUserPerm bitfield: 1=OWNER, 2=NONE, 4=MANAGER, 8=BOT. Forward-compatible with future values. */
110
+ open_permission: number | null
111
+ }
112
+
93
113
  export interface KakaoSendResult {
94
114
  success: boolean
95
115
  status_code: number
@@ -102,11 +122,13 @@ export const KakaoChatSchema = z.object({
102
122
  chat_id: z.string(),
103
123
  type: z.number(),
104
124
  display_name: z.string().nullable(),
125
+ title: z.string().nullable(),
105
126
  active_members: z.number(),
106
127
  unread_count: z.number(),
107
128
  last_message: z
108
129
  .object({
109
130
  author_id: z.number(),
131
+ author_name: z.string().nullable(),
110
132
  message: z.string(),
111
133
  sent_at: z.number(),
112
134
  })
@@ -117,10 +139,25 @@ export const KakaoMessageSchema = z.object({
117
139
  log_id: z.string(),
118
140
  type: z.number(),
119
141
  author_id: z.number(),
142
+ author_name: z.string().nullable(),
120
143
  message: z.string(),
121
144
  sent_at: z.number(),
122
145
  })
123
146
 
147
+ export const KakaoMemberSchema = z.object({
148
+ user_id: z.string(),
149
+ nickname: z.string(),
150
+ profile_image_url: z.string().nullable(),
151
+ full_profile_image_url: z.string().nullable(),
152
+ original_profile_image_url: z.string().nullable(),
153
+ status_message: z.string().nullable(),
154
+ country_iso: z.string().nullable(),
155
+ user_type: z.number().nullable(),
156
+ open_token: z.number().nullable(),
157
+ open_profile_link_id: z.string().nullable(),
158
+ open_permission: z.number().nullable(),
159
+ })
160
+
124
161
  export const KakaoSendResultSchema = z.object({
125
162
  success: z.boolean(),
126
163
  status_code: z.number(),
@@ -183,6 +220,7 @@ export interface KakaoTalkPushMessageEvent {
183
220
  chat_id: string
184
221
  log_id: string
185
222
  author_id: number
223
+ author_name: string | null
186
224
  message: string
187
225
  message_type: number
188
226
  sent_at: number
@@ -228,6 +266,7 @@ export const KakaoTalkPushMessageEventSchema = z.object({
228
266
  chat_id: z.string(),
229
267
  log_id: z.string(),
230
268
  author_id: z.number(),
269
+ author_name: z.string().nullable(),
231
270
  message: z.string(),
232
271
  message_type: z.number(),
233
272
  sent_at: z.number(),
@@ -243,6 +243,73 @@ describe('SlackBotClient', () => {
243
243
  expect(result.ts).toBe('1234567890.123456')
244
244
  expect(result.text).toBe('Hello')
245
245
  })
246
+
247
+ it('does not pass blocks/attachments when omitted', async () => {
248
+ // given
249
+ const client = await new SlackBotClient().login({ token: 'xoxb-test-token' })
250
+
251
+ // when
252
+ await client.postMessage('C123', 'Hello')
253
+
254
+ // then
255
+ expect(mockChat.postMessage).toHaveBeenCalledWith({
256
+ channel: 'C123',
257
+ text: 'Hello',
258
+ thread_ts: undefined,
259
+ blocks: undefined,
260
+ attachments: undefined,
261
+ unfurl_links: undefined,
262
+ unfurl_media: undefined,
263
+ mrkdwn: undefined,
264
+ })
265
+ })
266
+
267
+ it('forwards blocks to chat.postMessage', async () => {
268
+ // given
269
+ const client = await new SlackBotClient().login({ token: 'xoxb-test-token' })
270
+ const blocks = [
271
+ {
272
+ type: 'section',
273
+ text: { type: 'mrkdwn', text: '*hello*' },
274
+ },
275
+ ]
276
+
277
+ // when
278
+ await client.postMessage('C123', 'Hello', { blocks })
279
+
280
+ // then
281
+ expect(mockChat.postMessage).toHaveBeenCalledWith(
282
+ expect.objectContaining({ channel: 'C123', text: 'Hello', blocks }),
283
+ )
284
+ })
285
+
286
+ it('forwards thread_ts, attachments, and unfurl/mrkdwn flags', async () => {
287
+ // given
288
+ const client = await new SlackBotClient().login({ token: 'xoxb-test-token' })
289
+ const attachments = [{ text: 'attached' }]
290
+
291
+ // when
292
+ await client.postMessage('C123', 'Hello', {
293
+ thread_ts: '1234567890.000001',
294
+ attachments,
295
+ unfurl_links: false,
296
+ unfurl_media: false,
297
+ mrkdwn: true,
298
+ })
299
+
300
+ // then
301
+ expect(mockChat.postMessage).toHaveBeenCalledWith(
302
+ expect.objectContaining({
303
+ channel: 'C123',
304
+ text: 'Hello',
305
+ thread_ts: '1234567890.000001',
306
+ attachments,
307
+ unfurl_links: false,
308
+ unfurl_media: false,
309
+ mrkdwn: true,
310
+ }),
311
+ )
312
+ })
246
313
  })
247
314
 
248
315
  describe('getConversationHistory', () => {
@@ -103,12 +103,28 @@ export class SlackBotClient {
103
103
  })
104
104
  }
105
105
 
106
- async postMessage(channel: string, text: string, options?: { thread_ts?: string }): Promise<SlackMessage> {
106
+ async postMessage(
107
+ channel: string,
108
+ text: string,
109
+ options?: {
110
+ thread_ts?: string
111
+ blocks?: unknown[]
112
+ attachments?: unknown[]
113
+ unfurl_links?: boolean
114
+ unfurl_media?: boolean
115
+ mrkdwn?: boolean
116
+ },
117
+ ): Promise<SlackMessage> {
107
118
  return this.withRetry(async () => {
108
119
  const response = await this.ensureAuth().chat.postMessage({
109
120
  channel,
110
121
  text,
111
122
  thread_ts: options?.thread_ts,
123
+ blocks: options?.blocks as any,
124
+ attachments: options?.attachments as any,
125
+ unfurl_links: options?.unfurl_links,
126
+ unfurl_media: options?.unfurl_media,
127
+ mrkdwn: options?.mrkdwn,
112
128
  })
113
129
  this.checkResponse(response)
114
130
 
@@ -26,10 +26,10 @@ export class KakaoTalkAdapter implements PlatformAdapter {
26
26
 
27
27
  async getChannels(): Promise<UnifiedChannel[]> {
28
28
  const client = this.ensureClient()
29
- const chats = await client.getChats()
29
+ const chats = await client.getChats({ resolveTitles: true })
30
30
  return chats.map((chat) => ({
31
31
  id: chat.chat_id,
32
- name: chat.display_name || `Chat ${chat.chat_id}`,
32
+ name: chat.title || chat.display_name || `Chat ${chat.chat_id}`,
33
33
  }))
34
34
  }
35
35
 
@@ -39,7 +39,7 @@ export class KakaoTalkAdapter implements PlatformAdapter {
39
39
  return messages.map((msg) => ({
40
40
  id: msg.log_id,
41
41
  channelId,
42
- author: String(msg.author_id),
42
+ author: msg.author_name || String(msg.author_id),
43
43
  content: msg.message ?? '',
44
44
  timestamp: String(msg.sent_at),
45
45
  }))