agent-messenger 2.36.1 → 2.37.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 (58) 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 +5 -4
  4. package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -1
  5. package/dist/src/platforms/kakaotalk/chat-classifier.js +15 -9
  6. package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -1
  7. package/dist/src/platforms/kakaotalk/client.d.ts +33 -1
  8. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  9. package/dist/src/platforms/kakaotalk/client.js +377 -42
  10. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  11. package/dist/src/platforms/kakaotalk/index.d.ts +4 -3
  12. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  13. package/dist/src/platforms/kakaotalk/index.js +2 -2
  14. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  15. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  16. package/dist/src/platforms/kakaotalk/protocol/connection.js +13 -2
  17. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  18. package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts +1 -0
  19. package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts.map +1 -1
  20. package/dist/src/platforms/kakaotalk/protocol/login-response.js +1 -1
  21. package/dist/src/platforms/kakaotalk/protocol/login-response.js.map +1 -1
  22. package/dist/src/platforms/kakaotalk/types.d.ts +50 -2
  23. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  24. package/dist/src/platforms/kakaotalk/types.js +18 -1
  25. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  26. package/docs/content/docs/cli/kakaotalk.mdx +7 -1
  27. package/docs/content/docs/sdk/kakaotalk.mdx +29 -1
  28. package/package.json +1 -1
  29. package/skills/agent-channeltalk/SKILL.md +1 -1
  30. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  31. package/skills/agent-discord/SKILL.md +1 -1
  32. package/skills/agent-discordbot/SKILL.md +1 -1
  33. package/skills/agent-imessage/SKILL.md +1 -1
  34. package/skills/agent-instagram/SKILL.md +1 -1
  35. package/skills/agent-kakaotalk/SKILL.md +1 -1
  36. package/skills/agent-kakaotalk/references/common-patterns.md +36 -0
  37. package/skills/agent-line/SKILL.md +1 -1
  38. package/skills/agent-slack/SKILL.md +1 -1
  39. package/skills/agent-slackbot/SKILL.md +1 -1
  40. package/skills/agent-teams/SKILL.md +1 -1
  41. package/skills/agent-telegram/SKILL.md +1 -1
  42. package/skills/agent-telegrambot/SKILL.md +1 -1
  43. package/skills/agent-webex/SKILL.md +1 -1
  44. package/skills/agent-webexbot/SKILL.md +1 -1
  45. package/skills/agent-wechatbot/SKILL.md +1 -1
  46. package/skills/agent-whatsapp/SKILL.md +1 -1
  47. package/skills/agent-whatsappbot/SKILL.md +1 -1
  48. package/src/platforms/kakaotalk/chat-classifier.test.ts +23 -1
  49. package/src/platforms/kakaotalk/chat-classifier.ts +16 -10
  50. package/src/platforms/kakaotalk/client.test.ts +1225 -88
  51. package/src/platforms/kakaotalk/client.ts +476 -45
  52. package/src/platforms/kakaotalk/index.test.ts +39 -0
  53. package/src/platforms/kakaotalk/index.ts +12 -1
  54. package/src/platforms/kakaotalk/protocol/connection.test.ts +87 -0
  55. package/src/platforms/kakaotalk/protocol/connection.ts +15 -2
  56. package/src/platforms/kakaotalk/protocol/login-response.test.ts +13 -2
  57. package/src/platforms/kakaotalk/protocol/login-response.ts +1 -1
  58. package/src/platforms/kakaotalk/types.ts +41 -2
@@ -10,10 +10,12 @@ import {
10
10
  KakaoChatSchema,
11
11
  KakaoConfigSchema,
12
12
  KakaoLeaveChatResultSchema,
13
+ KakaoMessagePageSchema,
13
14
  KakaoMessageSchema,
14
15
  KakaoSendResultSchema,
15
16
  KakaoTalkClient,
16
17
  KakaoTalkError,
18
+ isOpenKakaoChatType,
17
19
  KakaoTalkListener,
18
20
  KakaoTalkPushMemberEventSchema,
19
21
  KakaoTalkPushMessageEventSchema,
@@ -47,10 +49,41 @@ it('KakaoChatSchema is exported from barrel', () => {
47
49
  expect(typeof KakaoChatSchema.parse).toBe('function')
48
50
  })
49
51
 
52
+ it('KakaoChatSchema preserves a non-empty string channel type', () => {
53
+ for (const channelType of ['DirectChat', 'MultiChat', 'PlusChat', 'MemoChat', 'OM', 'OD', 'FutureChat']) {
54
+ expect(
55
+ KakaoChatSchema.parse({
56
+ chat_id: '300',
57
+ type: channelType,
58
+ display_name: null,
59
+ title: null,
60
+ active_members: 2,
61
+ unread_count: 0,
62
+ last_message: null,
63
+ }).type,
64
+ ).toBe(channelType)
65
+ }
66
+ expect(() =>
67
+ KakaoChatSchema.parse({
68
+ chat_id: '300',
69
+ type: '',
70
+ display_name: null,
71
+ title: null,
72
+ active_members: 2,
73
+ unread_count: 0,
74
+ last_message: null,
75
+ }),
76
+ ).toThrow()
77
+ })
78
+
50
79
  it('KakaoMessageSchema is exported from barrel', () => {
51
80
  expect(typeof KakaoMessageSchema.parse).toBe('function')
52
81
  })
53
82
 
83
+ it('KakaoMessagePageSchema is exported from barrel', () => {
84
+ expect(typeof KakaoMessagePageSchema.parse).toBe('function')
85
+ })
86
+
54
87
  it('KakaoSendResultSchema is exported from barrel', () => {
55
88
  expect(typeof KakaoSendResultSchema.parse).toBe('function')
56
89
  })
@@ -83,6 +116,12 @@ it('classifyKakaoChat is exported from barrel', () => {
83
116
  expect(typeof classifyKakaoChat).toBe('function')
84
117
  })
85
118
 
119
+ it('isOpenKakaoChatType is exported from barrel', () => {
120
+ expect(isOpenKakaoChatType('OM')).toBe(true)
121
+ expect(isOpenKakaoChatType(13)).toBe(true)
122
+ expect(isOpenKakaoChatType('UNKNOWN')).toBe(false)
123
+ })
124
+
86
125
  it('KakaoLeaveChatResultSchema is exported from barrel', () => {
87
126
  expect(typeof KakaoLeaveChatResultSchema.parse).toBe('function')
88
127
  })
@@ -1,5 +1,10 @@
1
1
  export { KakaoTalkClient, KakaoTalkError } from './client'
2
- export { classifyKakaoChat } from './chat-classifier'
2
+ export type {
3
+ KakaoTalkGetChatFailureReason,
4
+ KakaoTalkResponseFailureKind,
5
+ KakaoTalkResponseStatusSource,
6
+ } from './client'
7
+ export { classifyKakaoChat, isOpenKakaoChatType } from './chat-classifier'
3
8
  export type { KakaoChatKind } from './chat-classifier'
4
9
  export { KakaoCredentialManager, CredentialManager } from './credential-manager'
5
10
  export { KakaoTalkListener } from './listener'
@@ -9,6 +14,8 @@ export type {
9
14
  KakaoAuthErrorCode,
10
15
  KakaoAuthMethod,
11
16
  KakaoChat,
17
+ KakaoChatType,
18
+ KakaoOpenChatType,
12
19
  KakaoConfig,
13
20
  KakaoDeviceType,
14
21
  KakaoEmoticonKind,
@@ -18,7 +25,9 @@ export type {
18
25
  KakaoLoginResult,
19
26
  KakaoMarkReadResult,
20
27
  KakaoMember,
28
+ KakaoMemberSnapshot,
21
29
  KakaoMessage,
30
+ KakaoMessagePage,
22
31
  KakaoMultiPhotoExtra,
23
32
  KakaoPhotoExtra,
24
33
  KakaoProfile,
@@ -44,7 +53,9 @@ export {
44
53
  KakaoLeaveChatResultSchema,
45
54
  KakaoMarkReadResultSchema,
46
55
  KakaoMemberSchema,
56
+ KakaoMemberSnapshotSchema,
47
57
  KakaoMessageSchema,
58
+ KakaoMessagePageSchema,
48
59
  KakaoProfileSchema,
49
60
  KakaoSendResultSchema,
50
61
  KakaoTalkPushEmoticonEventSchema,
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { LocoConnection } from './connection'
4
+ import { encodePacket } from './packet'
5
+ import type { LocoPacket } from './types'
6
+
7
+ interface ConnectionInternals {
8
+ socket: {
9
+ write: (data: Buffer, callback: (error?: Error) => void) => boolean
10
+ } | null
11
+ pendingResolvers: Map<number, unknown>
12
+ timedOutIds: Set<number>
13
+ onData: (chunk: Buffer) => void
14
+ }
15
+
16
+ function internals(connection: LocoConnection): ConnectionInternals {
17
+ return connection as unknown as ConnectionInternals
18
+ }
19
+
20
+ describe('LocoConnection.sendPacket', () => {
21
+ it('resolves a response delivered synchronously during write without double settlement', async () => {
22
+ const connection = new LocoConnection()
23
+ const owned = internals(connection)
24
+ const response: LocoPacket = {
25
+ packetId: 1,
26
+ statusCode: 0,
27
+ method: 'FAST',
28
+ bodyType: 0,
29
+ body: { accepted: true },
30
+ }
31
+ const pushed: LocoPacket[] = []
32
+ connection.onPush((packet) => pushed.push(packet))
33
+ owned.socket = {
34
+ write: (_data, callback) => {
35
+ owned.onData(encodePacket(response))
36
+ callback(new Error('late write callback failure'))
37
+ return false
38
+ },
39
+ }
40
+
41
+ await expect(connection.sendPacket('FAST')).resolves.toEqual(response)
42
+ expect(pushed).toEqual([])
43
+ expect(owned.pendingResolvers.size).toBe(0)
44
+ expect(owned.timedOutIds.size).toBe(0)
45
+ })
46
+
47
+ it('rejects a write failure and clears only its pending entry', async () => {
48
+ const connection = new LocoConnection()
49
+ const owned = internals(connection)
50
+ const pushed: LocoPacket[] = []
51
+ const sentinelTimer = setTimeout(() => {}, 60_000)
52
+ const sentinel = { resolve: () => {}, timer: sentinelTimer }
53
+ owned.pendingResolvers.set(99, sentinel)
54
+ connection.onPush((packet) => pushed.push(packet))
55
+ owned.socket = {
56
+ write: (_data, callback) => {
57
+ callback(new Error('socket write failed'))
58
+ return false
59
+ },
60
+ }
61
+
62
+ try {
63
+ await expect(connection.sendPacket('FAIL')).rejects.toThrow('socket write failed')
64
+ expect(owned.pendingResolvers.size).toBe(1)
65
+ expect(owned.pendingResolvers.get(99)).toBe(sentinel)
66
+ expect(owned.timedOutIds.has(1)).toBe(true)
67
+
68
+ owned.onData(
69
+ encodePacket({
70
+ packetId: 1,
71
+ statusCode: 0,
72
+ method: 'FAIL',
73
+ bodyType: 0,
74
+ body: { late: true },
75
+ }),
76
+ )
77
+
78
+ expect(pushed).toEqual([])
79
+ expect(owned.pendingResolvers.size).toBe(1)
80
+ expect(owned.pendingResolvers.get(99)).toBe(sentinel)
81
+ expect(owned.timedOutIds.size).toBe(0)
82
+ } finally {
83
+ clearTimeout(sentinelTimer)
84
+ owned.pendingResolvers.delete(99)
85
+ }
86
+ })
87
+ })
@@ -73,15 +73,28 @@ export class LocoConnection {
73
73
 
74
74
  const raw = encodePacket(packet)
75
75
  const data = this.crypto ? this.crypto.encrypt(raw) : raw
76
- await this.write(data)
77
76
 
78
77
  return new Promise((resolve, reject) => {
78
+ let entry: {
79
+ resolve: (packet: LocoPacket) => void
80
+ timer: ReturnType<typeof setTimeout>
81
+ }
79
82
  const timer = setTimeout(() => {
83
+ if (this.pendingResolvers.get(packetId) !== entry) return
80
84
  this.pendingResolvers.delete(packetId)
81
85
  this.timedOutIds.add(packetId)
82
86
  reject(new Error(`LOCO packet timeout: ${method} (${DEFAULT_SEND_TIMEOUT_MS}ms)`))
83
87
  }, DEFAULT_SEND_TIMEOUT_MS)
84
- this.pendingResolvers.set(packetId, { resolve, timer })
88
+ entry = { resolve, timer }
89
+ this.pendingResolvers.set(packetId, entry)
90
+
91
+ void this.write(data).catch((error: unknown) => {
92
+ if (this.pendingResolvers.get(packetId) !== entry) return
93
+ clearTimeout(entry.timer)
94
+ this.pendingResolvers.delete(packetId)
95
+ this.timedOutIds.add(packetId)
96
+ reject(error)
97
+ })
85
98
  })
86
99
  }
87
100
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
2
 
3
- import { validateLoginListResponse } from './login-response'
3
+ import { isSyntheticConnectionClose, validateLoginListResponse } from './login-response'
4
4
  import type { LocoPacket } from './types'
5
5
 
6
6
  function packet(statusCode: number, body: Record<string, unknown>): LocoPacket {
@@ -54,11 +54,22 @@ describe('LOGINLIST response classification', () => {
54
54
  })
55
55
 
56
56
  it('keeps a synthetic socket close on the transport-error path', () => {
57
- const error = captureError(() => validateLoginListResponse(packet(-1, { error: 'connection closed' })))
57
+ const response = packet(-1, { error: 'connection closed' })
58
+ const error = captureError(() => validateLoginListResponse(response))
58
59
 
60
+ expect(isSyntheticConnectionClose(response)).toBe(true)
59
61
  expect(error).toBeInstanceOf(Error)
60
62
  expect(error).toMatchObject({ message: 'KakaoTalk LOGINLIST transport failed: connection closed' })
61
63
  expect(error).not.toHaveProperty('code')
62
64
  expect(error).not.toHaveProperty('serverStatus')
63
65
  })
66
+
67
+ it('does not classify status -1 with a nonmatching body as a synthetic socket close', () => {
68
+ const response = packet(-1, { error: 'provider rejected' })
69
+
70
+ expect(isSyntheticConnectionClose(response)).toBe(false)
71
+ expect(() => validateLoginListResponse(response)).toThrow(
72
+ expect.objectContaining({ code: 'login_rejected', serverStatus: -1 }),
73
+ )
74
+ })
64
75
  })
@@ -17,7 +17,7 @@ export class KakaoLoginResponseError extends Error {
17
17
  }
18
18
  }
19
19
 
20
- function isSyntheticConnectionClose(response: LocoPacket): boolean {
20
+ export function isSyntheticConnectionClose(response: LocoPacket): boolean {
21
21
  return response.statusCode === CONNECTION_CLOSED_STATUS && response.body.error === 'connection closed'
22
22
  }
23
23
 
@@ -70,9 +70,20 @@ export const KAKAO_NEXT_ACTIONS: Record<string, { next_action: string; message:
70
70
  },
71
71
  }
72
72
 
73
+ export type KakaoOpenChatType = 2 | 13 | 14 | 15 | 16 | 'OM' | 'OD'
74
+ export const KNOWN_KAKAO_CHAT_STRING_TYPES = ['DirectChat', 'MultiChat', 'PlusChat', 'MemoChat', 'OM', 'OD'] as const
75
+ export type KakaoChatType = number | string
76
+
77
+ export function isKnownKakaoChatStringType(value: unknown): value is (typeof KNOWN_KAKAO_CHAT_STRING_TYPES)[number] {
78
+ return (
79
+ typeof value === 'string' &&
80
+ KNOWN_KAKAO_CHAT_STRING_TYPES.includes(value as (typeof KNOWN_KAKAO_CHAT_STRING_TYPES)[number])
81
+ )
82
+ }
83
+
73
84
  export interface KakaoChat {
74
85
  chat_id: string
75
- type: number
86
+ type: KakaoChatType
76
87
  display_name: string | null
77
88
  title: string | null
78
89
  active_members: number
@@ -95,6 +106,12 @@ export interface KakaoMessage {
95
106
  sent_at: number
96
107
  }
97
108
 
109
+ export interface KakaoMessagePage {
110
+ messages: KakaoMessage[]
111
+ next_cursor: string | null
112
+ complete: boolean
113
+ }
114
+
98
115
  export interface KakaoMember {
99
116
  user_id: string
100
117
  nickname: string
@@ -112,6 +129,14 @@ export interface KakaoMember {
112
129
  open_permission: number | null
113
130
  }
114
131
 
132
+ export interface KakaoMemberSnapshot {
133
+ chat_id: string
134
+ active_members: number
135
+ members: KakaoMember[]
136
+ complete: true
137
+ consistency_basis: 'stable_double_read_chatinfo_getmem'
138
+ }
139
+
115
140
  export interface KakaoSendResult {
116
141
  success: boolean
117
142
  status_code: number
@@ -222,7 +247,7 @@ export interface KakaoTypingResult {
222
247
 
223
248
  export const KakaoChatSchema = z.object({
224
249
  chat_id: z.string(),
225
- type: z.number(),
250
+ type: z.union([z.number(), z.string().min(1)]),
226
251
  display_name: z.string().nullable(),
227
252
  title: z.string().nullable(),
228
253
  active_members: z.number(),
@@ -247,6 +272,12 @@ export const KakaoMessageSchema = z.object({
247
272
  sent_at: z.number(),
248
273
  })
249
274
 
275
+ export const KakaoMessagePageSchema = z.object({
276
+ messages: z.array(KakaoMessageSchema),
277
+ next_cursor: z.string().nullable(),
278
+ complete: z.boolean(),
279
+ })
280
+
250
281
  export const KakaoMemberSchema = z.object({
251
282
  user_id: z.string(),
252
283
  nickname: z.string(),
@@ -261,6 +292,14 @@ export const KakaoMemberSchema = z.object({
261
292
  open_permission: z.number().nullable(),
262
293
  })
263
294
 
295
+ export const KakaoMemberSnapshotSchema = z.object({
296
+ chat_id: z.string(),
297
+ active_members: z.number().int().nonnegative(),
298
+ members: z.array(KakaoMemberSchema),
299
+ complete: z.literal(true),
300
+ consistency_basis: z.literal('stable_double_read_chatinfo_getmem'),
301
+ })
302
+
264
303
  export const KakaoSendResultSchema = z.object({
265
304
  success: z.boolean(),
266
305
  status_code: z.number(),