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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +18 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.js +29 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -0
- package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.js +2 -1
- package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +35 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +318 -15
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/chat.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/chat.js +2 -1
- package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/member.d.ts +3 -0
- package/dist/src/platforms/kakaotalk/commands/member.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/commands/member.js +22 -0
- package/dist/src/platforms/kakaotalk/commands/member.js.map +1 -0
- package/dist/src/platforms/kakaotalk/index.d.ts +4 -2
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +2 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.d.ts +4 -7
- package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.js +48 -74
- package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +28 -0
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +44 -0
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +37 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +17 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/slackbot/client.d.ts +5 -0
- package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/client.js +5 -0
- package/dist/src/platforms/slackbot/client.js.map +1 -1
- package/dist/src/tui/adapters/kakaotalk-adapter.js +3 -3
- package/dist/src/tui/adapters/kakaotalk-adapter.js.map +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +26 -1
- package/docs/content/docs/sdk/kakaotalk.mdx +45 -13
- package/docs/content/docs/sdk/slackbot.mdx +11 -0
- package/package.json +1 -1
- package/scripts/kakao-loco-capture.ts +466 -0
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +30 -3
- package/skills/agent-kakaotalk/references/common-patterns.md +49 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -2
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/kakaotalk/chat-classifier.test.ts +33 -0
- package/src/platforms/kakaotalk/chat-classifier.ts +31 -0
- package/src/platforms/kakaotalk/cli.ts +2 -1
- package/src/platforms/kakaotalk/client-listener-integration.test.ts +411 -0
- package/src/platforms/kakaotalk/client.test.ts +785 -1
- package/src/platforms/kakaotalk/client.ts +369 -18
- package/src/platforms/kakaotalk/commands/chat.ts +3 -1
- package/src/platforms/kakaotalk/commands/index.ts +1 -0
- package/src/platforms/kakaotalk/commands/member.test.ts +102 -0
- package/src/platforms/kakaotalk/commands/member.ts +32 -0
- package/src/platforms/kakaotalk/index.test.ts +5 -0
- package/src/platforms/kakaotalk/index.ts +4 -0
- package/src/platforms/kakaotalk/listener.test.ts +184 -149
- package/src/platforms/kakaotalk/listener.ts +51 -82
- package/src/platforms/kakaotalk/protocol/session.ts +44 -0
- package/src/platforms/kakaotalk/types.ts +39 -0
- package/src/platforms/slackbot/client.test.ts +67 -0
- package/src/platforms/slackbot/client.ts +17 -1
- 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.
|
|
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-kakaotalk
|
|
3
3
|
description: Interact with KakaoTalk - send messages, read chats, manage conversations
|
|
4
|
-
version: 2.
|
|
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":
|
|
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:
|
|
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-slackbot
|
|
3
3
|
description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
|
|
4
|
-
version: 2.
|
|
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
|
|