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
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * KakaoTalk LOCO wire-format capture (diagnostic only).
4
+ *
5
+ * Issues a small, throttled set of LOCO requests against a real account and
6
+ * writes the BSON response bodies to a temp file for offline schema inspection.
7
+ * PII is redacted (user IDs hashed, profile URLs scrubbed, message content
8
+ * elided, member nicknames replaced) before anything is written to disk.
9
+ *
10
+ * Defaults are intentionally conservative: 3 chats max, 1500ms between LOCO
11
+ * calls, dry-run unless `--confirm` is passed. The goal is to avoid bursting
12
+ * KakaoTalk's servers from a real account during local investigation.
13
+ *
14
+ * Usage:
15
+ * bun scripts/kakao-loco-capture.ts # Dry-run plan (no LOCO calls)
16
+ * bun scripts/kakao-loco-capture.ts --confirm # Execute with defaults
17
+ * bun scripts/kakao-loco-capture.ts --chat-id 123 --confirm
18
+ * bun scripts/kakao-loco-capture.ts --commands CHATINFO,CHATONROOM --confirm
19
+ *
20
+ * NOT shipped to npm consumers: this is a developer tool under scripts/, never
21
+ * imported by published code.
22
+ */
23
+
24
+ import { createHash } from 'node:crypto'
25
+ import { writeFileSync } from 'node:fs'
26
+ import { tmpdir } from 'node:os'
27
+ import { join } from 'node:path'
28
+
29
+ import { Long } from 'bson'
30
+
31
+ import { KakaoTalkClient } from '../src/platforms/kakaotalk/client'
32
+ import type { LocoSession } from '../src/platforms/kakaotalk/protocol/session'
33
+
34
+ const SUPPORTED_COMMANDS = ['CHATINFO', 'CHATONROOM', 'MEMBER', 'GETMEM', 'INFOLINK', 'LCHATLIST'] as const
35
+ type Command = (typeof SUPPORTED_COMMANDS)[number]
36
+
37
+ interface Args {
38
+ chatId: string | null
39
+ maxChats: number
40
+ delayMs: number
41
+ commands: Command[]
42
+ confirm: boolean
43
+ account: string | null
44
+ }
45
+
46
+ function parseArgs(argv: string[]): Args {
47
+ const args: Args = {
48
+ chatId: null,
49
+ maxChats: 3,
50
+ delayMs: 1500,
51
+ commands: [...SUPPORTED_COMMANDS],
52
+ confirm: false,
53
+ account: null,
54
+ }
55
+
56
+ for (let i = 0; i < argv.length; i++) {
57
+ const arg = argv[i]
58
+ if (arg === '--confirm') {
59
+ args.confirm = true
60
+ continue
61
+ }
62
+ const next = argv[i + 1]
63
+ if (arg === '--chat-id' && next) {
64
+ args.chatId = next
65
+ i++
66
+ } else if (arg === '--max-chats' && next) {
67
+ args.maxChats = Math.max(1, Number.parseInt(next, 10) || 1)
68
+ i++
69
+ } else if (arg === '--delay-ms' && next) {
70
+ args.delayMs = Math.max(0, Number.parseInt(next, 10) || 0)
71
+ i++
72
+ } else if (arg === '--commands' && next) {
73
+ const requested = next.split(',').map((s) => s.trim().toUpperCase())
74
+ const unknown = requested.filter((c) => !SUPPORTED_COMMANDS.includes(c as Command))
75
+ if (unknown.length > 0) {
76
+ throw new Error(`Unknown commands: ${unknown.join(', ')}. Supported: ${SUPPORTED_COMMANDS.join(', ')}`)
77
+ }
78
+ args.commands = requested as Command[]
79
+ i++
80
+ } else if (arg === '--account' && next) {
81
+ args.account = next
82
+ i++
83
+ } else if (arg === '--help' || arg === '-h') {
84
+ printHelp()
85
+ process.exit(0)
86
+ } else if (arg.startsWith('--')) {
87
+ throw new Error(`Unknown flag: ${arg}`)
88
+ }
89
+ }
90
+
91
+ return args
92
+ }
93
+
94
+ function printHelp(): void {
95
+ console.log(`
96
+ kakao-loco-capture — diagnostic LOCO wire dumper (developer tool)
97
+
98
+ Usage:
99
+ bun scripts/kakao-loco-capture.ts [options]
100
+
101
+ Options:
102
+ --chat-id <id> Probe only this chat (skips --max-chats limit for the chosen chat)
103
+ --max-chats <n> Number of chats to probe from your login snapshot (default: 3)
104
+ --delay-ms <n> Delay between LOCO calls in ms (default: 1500)
105
+ --commands <list> Comma-separated subset of: ${SUPPORTED_COMMANDS.join(', ')}
106
+ (default: all)
107
+ --account <id> Use a specific KakaoTalk account
108
+ --confirm Actually issue LOCO calls. Without this, prints the plan only.
109
+ --help, -h Show this help
110
+
111
+ Output: <tmpdir>/kakao-loco-capture-<timestamp>.json (PII redacted)
112
+ `)
113
+ }
114
+
115
+ async function sleep(ms: number): Promise<void> {
116
+ if (ms <= 0) return
117
+ await new Promise((resolve) => setTimeout(resolve, ms))
118
+ }
119
+
120
+ function hashUserId(id: unknown): string {
121
+ if (id === null || id === undefined) return '<null>'
122
+ return `user_${createHash('sha256').update(String(id)).digest('hex').slice(0, 8)}`
123
+ }
124
+
125
+ function isLong(v: unknown): v is { low: number; high: number } {
126
+ return typeof v === 'object' && v !== null && 'low' in v && 'high' in v
127
+ }
128
+
129
+ const URL_PATTERN = /https?:\/\/[^\s"'<>]+/gi
130
+ const USERID_KEYS = new Set(['userId', 'authorId', 'uid', 'i', 'mid', 'mids', 'memberIds', 'memberId', 'olu', 'opt'])
131
+ const CONTENT_KEYS = new Set(['message', 'msg', 'content', 'statusMessage', 'desc', 'description'])
132
+ // Keys whose string values are display names / nicknames. Replaced with a
133
+ // length-tagged placeholder so wire-shape inspection stays useful while the
134
+ // real names never reach disk.
135
+ const NAME_KEYS = new Set(['nickName', 'name', 'fullName', 'authorName', 'author_name', 'k', 'ln'])
136
+
137
+ function redactName(value: string): string {
138
+ return value.length > 0 ? `<name:${value.length}chars>` : '<name>'
139
+ }
140
+
141
+ function redact(value: unknown, key?: string): unknown {
142
+ if (value === null || value === undefined) return value
143
+ if (isLong(value)) {
144
+ if (key && USERID_KEYS.has(key)) return hashUserId(`${value.high}_${value.low}`)
145
+ return { __long: true, low: value.low, high: value.high }
146
+ }
147
+ if (typeof value === 'number') {
148
+ if (key && USERID_KEYS.has(key)) return hashUserId(value)
149
+ return value
150
+ }
151
+ if (typeof value === 'string') {
152
+ if (key && CONTENT_KEYS.has(key)) {
153
+ return value.length > 32 ? `<redacted:${value.length}chars>` : '<redacted>'
154
+ }
155
+ if (key && NAME_KEYS.has(key)) {
156
+ return redactName(value)
157
+ }
158
+ return value.replace(URL_PATTERN, '<url>')
159
+ }
160
+ if (typeof value === 'boolean') return value
161
+ if (Array.isArray(value)) {
162
+ // For NAME_KEYS that hold arrays of nicknames (notably `k`), redact each
163
+ // element as a name regardless of its position.
164
+ if (key && NAME_KEYS.has(key)) {
165
+ return value.map((v) => (typeof v === 'string' ? redactName(v) : redact(v, key)))
166
+ }
167
+ return value.map((v) => redact(v, key))
168
+ }
169
+ if (typeof value === 'object') {
170
+ const out: Record<string, unknown> = {}
171
+ for (const [k, v] of Object.entries(value)) {
172
+ out[k] = redact(v, k)
173
+ }
174
+ return out
175
+ }
176
+ return value
177
+ }
178
+
179
+ function parseLong(s: string): Long {
180
+ const big = BigInt(s)
181
+ const low = Number(big & 0xffffffffn)
182
+ const high = Number((big >> 32n) & 0xffffffffn)
183
+ return new Long(low, high)
184
+ }
185
+
186
+ function toLong(v: unknown): Long | null {
187
+ if (v && typeof v === 'object' && 'low' in v && 'high' in v) {
188
+ const { low, high } = v as { low: number; high: number }
189
+ return new Long(low, high)
190
+ }
191
+ if (typeof v === 'number' && Number.isFinite(v)) {
192
+ return Long.fromNumber(v)
193
+ }
194
+ return null
195
+ }
196
+
197
+ interface CaptureEntry {
198
+ command: Command
199
+ chatId: string
200
+ status: 'ok' | 'error'
201
+ status_code?: number
202
+ body?: unknown
203
+ error?: string
204
+ duration_ms: number
205
+ }
206
+
207
+ async function probe(
208
+ session: LocoSession,
209
+ command: Command,
210
+ chat: Record<string, unknown>,
211
+ delayMs: number,
212
+ ): Promise<CaptureEntry> {
213
+ const chatId = parseLong(String(chat.c))
214
+ const chatIdStr = String(chat.c)
215
+ const start = Date.now()
216
+
217
+ await sleep(delayMs)
218
+
219
+ try {
220
+ let body: Record<string, unknown>
221
+ let statusCode = 0
222
+
223
+ switch (command) {
224
+ case 'CHATINFO': {
225
+ const res = await session.getChannelInfo(chatId)
226
+ body = res.body
227
+ statusCode = res.statusCode
228
+ break
229
+ }
230
+ case 'CHATONROOM': {
231
+ const res = await session.getChatInfo(chatId)
232
+ body = res.body
233
+ statusCode = res.statusCode
234
+ break
235
+ }
236
+ case 'LCHATLIST': {
237
+ const res = await session.getChatList()
238
+ body = res.body
239
+ statusCode = res.statusCode
240
+ break
241
+ }
242
+ case 'MEMBER': {
243
+ const rawIds = (chat.i as Array<unknown> | undefined) ?? []
244
+ const sample: Long[] = []
245
+ for (const raw of rawIds.slice(0, 3)) {
246
+ const id = toLong(raw)
247
+ if (id) sample.push(id)
248
+ }
249
+ if (sample.length === 0) {
250
+ return {
251
+ command,
252
+ chatId: chatIdStr,
253
+ status: 'error',
254
+ error: 'no member ids available in chat data — skipping',
255
+ duration_ms: Date.now() - start,
256
+ }
257
+ }
258
+ const res = await session.getMembersByIds(chatId, sample)
259
+ body = res.body
260
+ statusCode = res.statusCode
261
+ break
262
+ }
263
+ case 'GETMEM': {
264
+ const res = await session.getAllMembers(chatId)
265
+ body = res.body
266
+ statusCode = res.statusCode
267
+ break
268
+ }
269
+ case 'INFOLINK': {
270
+ const linkId = toLong(chat.li)
271
+ if (!linkId) {
272
+ return {
273
+ command,
274
+ chatId: chatIdStr,
275
+ status: 'error',
276
+ error: 'not an open chat (no `li` field) — skipping',
277
+ duration_ms: Date.now() - start,
278
+ }
279
+ }
280
+ const res = await session.getOpenLinkInfo([linkId])
281
+ body = res.body
282
+ statusCode = res.statusCode
283
+ break
284
+ }
285
+ }
286
+
287
+ return {
288
+ command,
289
+ chatId: chatIdStr,
290
+ status: 'ok',
291
+ status_code: statusCode,
292
+ body: redact(body),
293
+ duration_ms: Date.now() - start,
294
+ }
295
+ } catch (error) {
296
+ return {
297
+ command,
298
+ chatId: chatIdStr,
299
+ status: 'error',
300
+ error: error instanceof Error ? error.message : String(error),
301
+ duration_ms: Date.now() - start,
302
+ }
303
+ }
304
+ }
305
+
306
+ function selfCheckRedact(): void {
307
+ // Defensive: any change that breaks redaction would silently leak PII into the
308
+ // capture file. Verify on every run against a known fixture before any LOCO
309
+ // calls happen. Fail loud if the contract changes.
310
+ const fixture = {
311
+ chatInfo: {
312
+ type: 'MultiChat',
313
+ chatMetas: [
314
+ { type: 3, content: 'My Group' },
315
+ { type: 1, content: 'Pinned message body' },
316
+ ],
317
+ displayMembers: [{ userId: 42, nickName: 'Alice', profileImageUrl: 'https://kakao.com/p/alice.jpg' }],
318
+ },
319
+ authorId: 1234567890,
320
+ message: 'secret hi',
321
+ k: ['Alice', 'Bob', 'Charlie'],
322
+ ln: 'My OpenChat Room',
323
+ name: 'Display Name',
324
+ fullName: 'Real Name',
325
+ l: { low: 999, high: 0 },
326
+ }
327
+ const out = redact(fixture) as Record<string, unknown>
328
+ const info = out.chatInfo as Record<string, unknown>
329
+ const display = (info.displayMembers as Array<Record<string, unknown>>)[0]
330
+ const metas = info.chatMetas as Array<Record<string, unknown>>
331
+
332
+ const failures: string[] = []
333
+ if (out.authorId === 1234567890) failures.push('authorId not hashed')
334
+ if (display.userId === 42) failures.push('displayMembers[].userId not hashed')
335
+ if ((display.profileImageUrl as string) !== '<url>') failures.push('profileImageUrl not scrubbed')
336
+ if ((display.nickName as string) === 'Alice') failures.push('displayMembers[].nickName not redacted')
337
+ if (out.message !== '<redacted>') failures.push('message content not redacted')
338
+ if (metas[0].content !== '<redacted>') failures.push('chatMetas content not redacted')
339
+ const memberNames = out.k as unknown[]
340
+ if (!Array.isArray(memberNames) || memberNames.some((n) => typeof n === 'string' && n === 'Alice')) {
341
+ failures.push('member name array (k) not redacted')
342
+ }
343
+ if (out.ln === 'My OpenChat Room') failures.push('open-link name (ln) not redacted')
344
+ if (out.name === 'Display Name') failures.push('display name not redacted')
345
+ if (out.fullName === 'Real Name') failures.push('fullName not redacted')
346
+ if (typeof out.l !== 'object' || (out.l as Record<string, unknown>).__long !== true) {
347
+ failures.push('non-user Long not preserved as tagged shape')
348
+ }
349
+
350
+ if (failures.length > 0) {
351
+ throw new Error(`redact() self-check failed:\n - ${failures.join('\n - ')}`)
352
+ }
353
+ }
354
+
355
+ async function main(): Promise<void> {
356
+ selfCheckRedact()
357
+
358
+ const args = parseArgs(process.argv.slice(2))
359
+
360
+ console.log('kakao-loco-capture — plan:')
361
+ console.log(` max_chats: ${args.maxChats}`)
362
+ console.log(` delay_ms: ${args.delayMs}`)
363
+ console.log(` commands: ${args.commands.join(', ')}`)
364
+ console.log(` chat_id: ${args.chatId ?? '<from login snapshot>'}`)
365
+ console.log(` account: ${args.account ? '<set>' : '<current>'}`)
366
+ console.log(` confirm: ${args.confirm}`)
367
+ console.log()
368
+
369
+ if (!args.confirm) {
370
+ console.log('Dry run (no --confirm). Re-run with --confirm to actually issue LOCO calls.')
371
+ return
372
+ }
373
+
374
+ const client = new KakaoTalkClient()
375
+
376
+ // Signal handler: `finally` does not run on SIGINT/SIGTERM, so wire explicit
377
+ // cleanup. Without this, Ctrl-C during a long sleep() leaks the LOCO socket
378
+ // and ping timer.
379
+ let cleaningUp = false
380
+ const cleanup = (signal: string) => {
381
+ if (cleaningUp) return
382
+ cleaningUp = true
383
+ console.error(`\nReceived ${signal}, closing LOCO session...`)
384
+ try {
385
+ client.close()
386
+ } catch {}
387
+ process.exit(130)
388
+ }
389
+ process.on('SIGINT', () => cleanup('SIGINT'))
390
+ process.on('SIGTERM', () => cleanup('SIGTERM'))
391
+
392
+ await client.login(undefined, args.account ?? undefined)
393
+
394
+ try {
395
+ const allChats = await client.getChats()
396
+ if (allChats.length === 0) {
397
+ console.error('No chats in login snapshot. Aborting.')
398
+ return
399
+ }
400
+
401
+ const session = await client.acquireSession()
402
+
403
+ const sessionState = (
404
+ client as unknown as { state: { loginResult: { chatDatas?: Array<Record<string, unknown>> } } }
405
+ ).state
406
+ const chatDatas = sessionState?.loginResult?.chatDatas ?? []
407
+
408
+ let targets: Array<Record<string, unknown>>
409
+ if (args.chatId) {
410
+ const match = chatDatas.find((c) => String(c.c) === args.chatId)
411
+ if (!match) {
412
+ console.error(`chat ${args.chatId} not in login snapshot`)
413
+ return
414
+ }
415
+ targets = [match]
416
+ } else {
417
+ targets = chatDatas.slice(0, args.maxChats)
418
+ }
419
+
420
+ console.log(`Probing ${targets.length} chat(s) with ${args.commands.length} command(s) each...`)
421
+ console.log(`Estimated wire calls: ${targets.length * args.commands.length}`)
422
+ console.log(`Estimated duration: ~${Math.ceil((targets.length * args.commands.length * args.delayMs) / 1000)}s`)
423
+ console.log()
424
+
425
+ const entries: CaptureEntry[] = []
426
+
427
+ if (args.commands.includes('LCHATLIST')) {
428
+ entries.push({
429
+ command: 'LCHATLIST',
430
+ chatId: '<login_snapshot>',
431
+ status: 'ok',
432
+ body: redact({ chatDatas: chatDatas.slice(0, args.maxChats) }),
433
+ duration_ms: 0,
434
+ })
435
+ }
436
+
437
+ for (const chat of targets) {
438
+ for (const command of args.commands) {
439
+ if (command === 'LCHATLIST') continue
440
+ const result = await probe(session, command, chat, args.delayMs)
441
+ const tag = result.status === 'ok' ? 'ok ' : 'ERR'
442
+ console.log(
443
+ ` [${tag}] ${command.padEnd(10)} chat=${result.chatId} ${result.duration_ms}ms${result.error ? `: ${result.error}` : ''}`,
444
+ )
445
+ entries.push(result)
446
+ }
447
+ }
448
+
449
+ const outPath = join(tmpdir(), `kakao-loco-capture-${new Date().toISOString().replace(/[:.]/g, '-')}.json`)
450
+ const payload = {
451
+ captured_at: new Date().toISOString(),
452
+ args: { ...args, account: args.account ? '<set>' : null, chatId: args.chatId ? '<set>' : null },
453
+ entries,
454
+ }
455
+ writeFileSync(outPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 })
456
+ console.log()
457
+ console.log(`Wrote ${entries.length} entries to ${outPath}`)
458
+ } finally {
459
+ client.close()
460
+ }
461
+ }
462
+
463
+ main().catch((error) => {
464
+ console.error('kakao-loco-capture failed:', error instanceof Error ? error.message : error)
465
+ process.exit(1)
466
+ })
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-channeltalk
3
3
  description: Interact with Channel Talk using extracted desktop app or browser credentials - read chats, send messages, search messages, manage groups
4
- version: 2.12.1
4
+ version: 2.13.0
5
5
  allowed-tools: Bash(agent-channeltalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-channeltalkbot
3
3
  description: Interact with Channel Talk workspaces using API credentials - send messages, read chats, manage groups and bots
4
- version: 2.12.1
4
+ version: 2.13.0
5
5
  allowed-tools: Bash(agent-channeltalkbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-discord
3
3
  description: Interact with Discord servers - send messages, read channels, manage reactions
4
- version: 2.12.1
4
+ version: 2.13.0
5
5
  allowed-tools: Bash(agent-discord:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-discordbot
3
3
  description: Interact with Discord servers using bot tokens - send messages, read channels, manage reactions
4
- version: 2.12.1
4
+ version: 2.13.0
5
5
  allowed-tools: Bash(agent-discordbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-instagram
3
3
  description: Interact with Instagram DMs - send messages, read conversations, manage accounts
4
- version: 2.12.1
4
+ version: 2.13.0
5
5
  allowed-tools: Bash(agent-instagram:*)
6
6
  metadata:
7
7
  openclaw:
@@ -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.1
4
+ version: 2.13.0
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.1
4
+ version: 2.13.0
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.1
4
+ version: 2.13.0
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.1
4
+ version: 2.13.0
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