agent-messenger 2.33.0 → 2.35.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 (97) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +12 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/discord/client.d.ts +16 -1
  5. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  6. package/dist/src/platforms/discord/client.js +50 -3
  7. package/dist/src/platforms/discord/client.js.map +1 -1
  8. package/dist/src/platforms/discord/commands/dm.d.ts +4 -0
  9. package/dist/src/platforms/discord/commands/dm.d.ts.map +1 -1
  10. package/dist/src/platforms/discord/commands/dm.js +56 -4
  11. package/dist/src/platforms/discord/commands/dm.js.map +1 -1
  12. package/dist/src/platforms/discord/index.d.ts +1 -1
  13. package/dist/src/platforms/discord/index.d.ts.map +1 -1
  14. package/dist/src/platforms/discord/index.js.map +1 -1
  15. package/dist/src/platforms/discord/listener.d.ts +4 -2
  16. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  17. package/dist/src/platforms/discord/listener.js +9 -2
  18. package/dist/src/platforms/discord/listener.js.map +1 -1
  19. package/dist/src/platforms/discord/types.d.ts +19 -0
  20. package/dist/src/platforms/discord/types.d.ts.map +1 -1
  21. package/dist/src/platforms/discord/types.js +5 -1
  22. package/dist/src/platforms/discord/types.js.map +1 -1
  23. package/dist/src/platforms/kakaotalk/auth/kakao-login.d.ts +2 -0
  24. package/dist/src/platforms/kakaotalk/auth/kakao-login.d.ts.map +1 -1
  25. package/dist/src/platforms/kakaotalk/auth/kakao-login.js +2 -2
  26. package/dist/src/platforms/kakaotalk/auth/kakao-login.js.map +1 -1
  27. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.d.ts +28 -0
  28. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.d.ts.map +1 -0
  29. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.js +100 -0
  30. package/dist/src/platforms/kakaotalk/auth/oauth-refresh.js.map +1 -0
  31. package/dist/src/platforms/kakaotalk/client.d.ts +2 -0
  32. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  33. package/dist/src/platforms/kakaotalk/client.js +14 -1
  34. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  35. package/dist/src/platforms/kakaotalk/index.d.ts +3 -1
  36. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  37. package/dist/src/platforms/kakaotalk/index.js +1 -0
  38. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  39. package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts +9 -0
  40. package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts.map +1 -0
  41. package/dist/src/platforms/kakaotalk/protocol/login-response.js +32 -0
  42. package/dist/src/platforms/kakaotalk/protocol/login-response.js.map +1 -0
  43. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  44. package/dist/src/platforms/kakaotalk/protocol/session.js +29 -20
  45. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  46. package/dist/src/platforms/kakaotalk/protocol/types.d.ts +1 -0
  47. package/dist/src/platforms/kakaotalk/protocol/types.d.ts.map +1 -1
  48. package/dist/src/platforms/kakaotalk/protocol/types.js.map +1 -1
  49. package/dist/src/platforms/kakaotalk/types.d.ts +1 -0
  50. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  51. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  52. package/docs/content/docs/cli/discord.mdx +17 -0
  53. package/docs/content/docs/cli/kakaotalk.mdx +10 -0
  54. package/docs/content/docs/sdk/discord.mdx +13 -0
  55. package/docs/content/docs/sdk/kakaotalk.mdx +54 -0
  56. package/package.json +1 -1
  57. package/skills/agent-channeltalk/SKILL.md +1 -1
  58. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  59. package/skills/agent-discord/SKILL.md +23 -1
  60. package/skills/agent-discordbot/SKILL.md +1 -1
  61. package/skills/agent-imessage/SKILL.md +1 -1
  62. package/skills/agent-instagram/SKILL.md +1 -1
  63. package/skills/agent-kakaotalk/SKILL.md +27 -1
  64. package/skills/agent-kakaotalk/references/authentication.md +59 -0
  65. package/skills/agent-kakaotalk/references/common-patterns.md +8 -0
  66. package/skills/agent-line/SKILL.md +1 -1
  67. package/skills/agent-slack/SKILL.md +1 -1
  68. package/skills/agent-slackbot/SKILL.md +1 -1
  69. package/skills/agent-teams/SKILL.md +1 -1
  70. package/skills/agent-telegram/SKILL.md +1 -1
  71. package/skills/agent-telegrambot/SKILL.md +1 -1
  72. package/skills/agent-webex/SKILL.md +1 -1
  73. package/skills/agent-webexbot/SKILL.md +1 -1
  74. package/skills/agent-wechatbot/SKILL.md +1 -1
  75. package/skills/agent-whatsapp/SKILL.md +1 -1
  76. package/skills/agent-whatsappbot/SKILL.md +1 -1
  77. package/src/platforms/discord/client.test.ts +151 -1
  78. package/src/platforms/discord/client.ts +76 -3
  79. package/src/platforms/discord/commands/dm.test.ts +126 -1
  80. package/src/platforms/discord/commands/dm.ts +63 -3
  81. package/src/platforms/discord/index.ts +3 -0
  82. package/src/platforms/discord/listener.test.ts +12 -3
  83. package/src/platforms/discord/listener.ts +17 -4
  84. package/src/platforms/discord/types.ts +37 -1
  85. package/src/platforms/kakaotalk/auth/kakao-login.ts +2 -2
  86. package/src/platforms/kakaotalk/auth/oauth-refresh.test.ts +225 -0
  87. package/src/platforms/kakaotalk/auth/oauth-refresh.ts +144 -0
  88. package/src/platforms/kakaotalk/client.test.ts +55 -0
  89. package/src/platforms/kakaotalk/client.ts +19 -2
  90. package/src/platforms/kakaotalk/index.test.ts +11 -0
  91. package/src/platforms/kakaotalk/index.ts +8 -0
  92. package/src/platforms/kakaotalk/protocol/NOTICE.md +19 -4
  93. package/src/platforms/kakaotalk/protocol/login-response.test.ts +64 -0
  94. package/src/platforms/kakaotalk/protocol/login-response.ts +40 -0
  95. package/src/platforms/kakaotalk/protocol/session.ts +29 -21
  96. package/src/platforms/kakaotalk/protocol/types.ts +1 -0
  97. package/src/platforms/kakaotalk/types.ts +1 -0
@@ -144,6 +144,61 @@ describe('KakaoTalkClient', () => {
144
144
  expect((e as KakaoTalkError).code).toBe('missing_user_id')
145
145
  }
146
146
  })
147
+
148
+ it('preserves invalid_access_token and never continues to LCHATLIST', async () => {
149
+ mockLogin.mockRejectedValue(
150
+ Object.assign(new Error('KakaoTalk LOGINLIST invalid access token (status -950)'), {
151
+ code: 'invalid_access_token',
152
+ serverStatus: -950,
153
+ }),
154
+ )
155
+ const client = await new KakaoTalkClient().login({
156
+ oauthToken: 'expired-access-token',
157
+ userId: 'user1',
158
+ deviceUuid: 'device1',
159
+ })
160
+
161
+ let error: unknown
162
+ try {
163
+ await client.getChats({ all: true })
164
+ } catch (cause) {
165
+ error = cause
166
+ }
167
+
168
+ expect(error).toBeInstanceOf(KakaoTalkError)
169
+ expect(error).toMatchObject({ code: 'invalid_access_token', serverStatus: -950 })
170
+ expect(mockGetChatList).not.toHaveBeenCalled()
171
+ expect(mockClose).toHaveBeenCalledTimes(1)
172
+ })
173
+
174
+ it('preserves other typed LOGINLIST rejections', async () => {
175
+ mockLogin.mockRejectedValue(
176
+ Object.assign(new Error('KakaoTalk LOGINLIST rejected (status -777)'), {
177
+ code: 'login_rejected',
178
+ serverStatus: -777,
179
+ }),
180
+ )
181
+ const client = await new KakaoTalkClient().login({
182
+ oauthToken: 'rejected-access-token',
183
+ userId: 'user1',
184
+ deviceUuid: 'device1',
185
+ })
186
+
187
+ await expect(client.getChats()).rejects.toMatchObject({ code: 'login_rejected', serverStatus: -777 })
188
+ expect(mockGetChatList).not.toHaveBeenCalled()
189
+ })
190
+
191
+ it('keeps interrupted LOGINLIST connections classified as login_failed', async () => {
192
+ mockLogin.mockRejectedValue(new Error('KakaoTalk LOGINLIST transport failed: connection closed'))
193
+ const client = await new KakaoTalkClient().login({
194
+ oauthToken: 'token',
195
+ userId: 'user1',
196
+ deviceUuid: 'device1',
197
+ })
198
+
199
+ await expect(client.getChats()).rejects.toMatchObject({ code: 'login_failed' })
200
+ expect(mockGetChatList).not.toHaveBeenCalled()
201
+ })
147
202
  })
148
203
 
149
204
  describe('getChats', () => {
@@ -40,11 +40,13 @@ export type KakaoSessionEventHandler = (event: KakaoSessionEvent) => void
40
40
 
41
41
  export class KakaoTalkError extends Error {
42
42
  code: string
43
+ readonly serverStatus?: number
43
44
 
44
- constructor(message: string, code: string, options?: { cause?: unknown }) {
45
+ constructor(message: string, code: string, options?: { cause?: unknown; serverStatus?: number }) {
45
46
  super(message, options)
46
47
  this.name = 'KakaoTalkError'
47
48
  this.code = code
49
+ this.serverStatus = options?.serverStatus
48
50
  }
49
51
  }
50
52
 
@@ -280,6 +282,17 @@ function wrapError(error: unknown, code: string): KakaoTalkError {
280
282
  return new KakaoTalkError(message, code, { cause: error })
281
283
  }
282
284
 
285
+ function isLoginResponseError(
286
+ error: unknown,
287
+ ): error is Error & { code: 'invalid_access_token' | 'login_rejected'; serverStatus: number } {
288
+ if (!(error instanceof Error)) return false
289
+ const candidate = error as Error & { code?: unknown; serverStatus?: unknown }
290
+ return (
291
+ (candidate.code === 'invalid_access_token' || candidate.code === 'login_rejected') &&
292
+ typeof candidate.serverStatus === 'number'
293
+ )
294
+ }
295
+
283
296
  const MAX_PAGES = 50
284
297
 
285
298
  function syncStatePath(deviceUuid: string): string {
@@ -675,7 +688,11 @@ export class KakaoTalkClient {
675
688
  return { session, loginResult }
676
689
  } catch (error) {
677
690
  session.close()
678
- throw new KakaoTalkError(error instanceof Error ? error.message : String(error), 'login_failed', { cause: error })
691
+ const code = isLoginResponseError(error) ? error.code : 'login_failed'
692
+ throw new KakaoTalkError(error instanceof Error ? error.message : String(error), code, {
693
+ cause: error,
694
+ serverStatus: isLoginResponseError(error) ? error.serverStatus : undefined,
695
+ })
679
696
  }
680
697
  }
681
698
 
@@ -1,8 +1,10 @@
1
1
  import { expect, it } from 'bun:test'
2
2
 
3
+ import type { KakaoOAuthRefreshOptions } from '@/platforms/kakaotalk/index'
3
4
  import {
4
5
  classifyKakaoChat,
5
6
  CredentialManager,
7
+ KakaoOAuthRefreshError,
6
8
  KakaoAccountCredentialsSchema,
7
9
  KakaoCredentialManager,
8
10
  KakaoChatSchema,
@@ -18,6 +20,7 @@ import {
18
20
  KakaoProfileSchema,
19
21
  KakaoTalkPushReadEventSchema,
20
22
  KakaoTypingResultSchema,
23
+ refreshKakaoOAuthToken,
21
24
  } from '@/platforms/kakaotalk/index'
22
25
 
23
26
  it('KakaoTalkClient is exported from barrel', () => {
@@ -87,3 +90,11 @@ it('KakaoLeaveChatResultSchema is exported from barrel', () => {
87
90
  it('KakaoTypingResultSchema is exported from barrel', () => {
88
91
  expect(typeof KakaoTypingResultSchema.parse).toBe('function')
89
92
  })
93
+
94
+ it('Kakao OAuth refresh API is exported from barrel', () => {
95
+ expect(typeof refreshKakaoOAuthToken).toBe('function')
96
+ expect(typeof KakaoOAuthRefreshError).toBe('function')
97
+
98
+ const options: KakaoOAuthRefreshOptions = { timeoutMs: 5_000 }
99
+ expect(options.timeoutMs).toBe(5_000)
100
+ })
@@ -6,6 +6,7 @@ export { KakaoTalkListener } from './listener'
6
6
  export type { PendingLoginState } from './credential-manager'
7
7
  export type {
8
8
  KakaoAccountCredentials,
9
+ KakaoAuthErrorCode,
9
10
  KakaoAuthMethod,
10
11
  KakaoChat,
11
12
  KakaoConfig,
@@ -54,6 +55,13 @@ export {
54
55
  } from './types'
55
56
  export { attemptLogin, generateDeviceUuid, loginFlow, registerDevice, requestPasscode } from './auth/kakao-login'
56
57
  export type { LoginCredentials } from './auth/kakao-login'
58
+ export { KakaoOAuthRefreshError, refreshKakaoOAuthToken } from './auth/oauth-refresh'
59
+ export type {
60
+ KakaoOAuthRefreshErrorCode,
61
+ KakaoOAuthRefreshInput,
62
+ KakaoOAuthRefreshOptions,
63
+ KakaoOAuthRefreshResult,
64
+ } from './auth/oauth-refresh'
57
65
  export { sha1Hex } from './media-upload'
58
66
  export { detectImageDimensions } from './image-meta'
59
67
  export type { AttachmentInput, AttachmentPlan, ResolvedAttachment, SingleAttachmentKind } from './attachment-router'
@@ -1,8 +1,9 @@
1
1
  # Third-Party Notices
2
2
 
3
- This LOCO protocol implementation was written from scratch for agent-messenger.
4
- The protocol knowledge used to build it comes from the following open-source
5
- projects and their documentation. No code was copied from these projects.
3
+ This KakaoTalk protocol and Android authentication implementation was written
4
+ from scratch for agent-messenger. The protocol knowledge used to build it comes
5
+ from the following open-source projects and their documentation. No code was
6
+ copied from these projects.
6
7
 
7
8
  ---
8
9
 
@@ -37,7 +38,21 @@ Android sub-device login flow, and connection patterns.
37
38
  - Copyright (c) 2020 storycraft
38
39
 
39
40
  Referenced for: LOCO packet format, BSON command schemas, authentication flow,
40
- X-VC signature algorithm, channel/chat type enumerations.
41
+ Android OAuth refresh request/response behavior, X-VC signature algorithm,
42
+ channel/chat type enumerations.
43
+
44
+ ---
45
+
46
+ ## KakaoForge
47
+
48
+ - Repository: https://github.com/play2fly/KakaoForge
49
+ - Reference commit: 4b774ea40b1347280fadb685415436584093118b
50
+ - License: KakaoForge Non-Commercial / No Abuse License
51
+ - Copyright (c) 2026 aodjo
52
+
53
+ Referenced for: current Android `oauth2_token.json` request fields, client
54
+ headers, device-id handling, and token rotation/expiry response fields. Protocol
55
+ behavior only; no source code was copied or adapted.
41
56
 
42
57
  ---
43
58
 
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { validateLoginListResponse } from './login-response'
4
+ import type { LocoPacket } from './types'
5
+
6
+ function packet(statusCode: number, body: Record<string, unknown>): LocoPacket {
7
+ return { packetId: 1, statusCode, method: 'LOGINLIST', bodyType: 0, body }
8
+ }
9
+
10
+ function captureError(fn: () => unknown): unknown {
11
+ try {
12
+ fn()
13
+ return undefined
14
+ } catch (error) {
15
+ return error
16
+ }
17
+ }
18
+
19
+ describe('LOGINLIST response classification', () => {
20
+ it('accepts status=0 and returns the login snapshot', () => {
21
+ const body = { status: 0, chatDatas: [], eof: true }
22
+
23
+ expect(validateLoginListResponse(packet(0, body))).toBe(body)
24
+ })
25
+
26
+ it('accepts a successful response when body.status is omitted', () => {
27
+ const body = { chatDatas: [], eof: true }
28
+
29
+ expect(validateLoginListResponse(packet(0, body))).toBe(body)
30
+ })
31
+
32
+ it('classifies body.status=-950 as invalid_access_token', () => {
33
+ expect(() => validateLoginListResponse(packet(0, { status: -950 }))).toThrow(
34
+ expect.objectContaining({ code: 'invalid_access_token', serverStatus: -950 }),
35
+ )
36
+ })
37
+
38
+ it('classifies packet statusCode=-950 as invalid_access_token', () => {
39
+ expect(() => validateLoginListResponse(packet(-950, {}))).toThrow(
40
+ expect.objectContaining({ code: 'invalid_access_token', serverStatus: -950 }),
41
+ )
42
+ })
43
+
44
+ it('classifies an unknown non-zero body status as login_rejected', () => {
45
+ expect(() => validateLoginListResponse(packet(0, { status: -777 }))).toThrow(
46
+ expect.objectContaining({ code: 'login_rejected', serverStatus: -777 }),
47
+ )
48
+ })
49
+
50
+ it('classifies an unknown non-zero packet status as login_rejected', () => {
51
+ expect(() => validateLoginListResponse(packet(-777, {}))).toThrow(
52
+ expect.objectContaining({ code: 'login_rejected', serverStatus: -777 }),
53
+ )
54
+ })
55
+
56
+ it('keeps a synthetic socket close on the transport-error path', () => {
57
+ const error = captureError(() => validateLoginListResponse(packet(-1, { error: 'connection closed' })))
58
+
59
+ expect(error).toBeInstanceOf(Error)
60
+ expect(error).toMatchObject({ message: 'KakaoTalk LOGINLIST transport failed: connection closed' })
61
+ expect(error).not.toHaveProperty('code')
62
+ expect(error).not.toHaveProperty('serverStatus')
63
+ })
64
+ })
@@ -0,0 +1,40 @@
1
+ import type { KakaoAuthErrorCode } from '../types'
2
+ import type { LoginListResponse, LocoPacket } from './types'
3
+
4
+ const CONNECTION_CLOSED_STATUS = -1
5
+ const INVALID_ACCESS_TOKEN_STATUS = -950
6
+
7
+ export class KakaoLoginResponseError extends Error {
8
+ readonly code: KakaoAuthErrorCode
9
+ readonly serverStatus: number
10
+
11
+ constructor(code: KakaoAuthErrorCode, serverStatus: number) {
12
+ const reason = code === 'invalid_access_token' ? 'invalid access token' : 'rejected'
13
+ super(`KakaoTalk LOGINLIST ${reason} (status ${serverStatus})`)
14
+ this.name = 'KakaoLoginResponseError'
15
+ this.code = code
16
+ this.serverStatus = serverStatus
17
+ }
18
+ }
19
+
20
+ function isSyntheticConnectionClose(response: LocoPacket): boolean {
21
+ return response.statusCode === CONNECTION_CLOSED_STATUS && response.body.error === 'connection closed'
22
+ }
23
+
24
+ function throwAuthenticationError(serverStatus: number): never {
25
+ const code: KakaoAuthErrorCode =
26
+ serverStatus === INVALID_ACCESS_TOKEN_STATUS ? 'invalid_access_token' : 'login_rejected'
27
+ throw new KakaoLoginResponseError(code, serverStatus)
28
+ }
29
+
30
+ export function validateLoginListResponse(response: LocoPacket): LoginListResponse {
31
+ if (isSyntheticConnectionClose(response)) {
32
+ throw new Error('KakaoTalk LOGINLIST transport failed: connection closed')
33
+ }
34
+
35
+ const bodyStatus = typeof response.body.status === 'number' ? response.body.status : 0
36
+ if (bodyStatus !== 0) throwAuthenticationError(bodyStatus)
37
+ if (response.statusCode !== 0) throwAuthenticationError(response.statusCode)
38
+
39
+ return response.body as unknown as LoginListResponse
40
+ }
@@ -15,6 +15,7 @@ import {
15
15
  getLocoDeviceConfig,
16
16
  } from './config'
17
17
  import { LocoConnection } from './connection'
18
+ import { validateLoginListResponse } from './login-response'
18
19
  import type { BookingResponse, CheckinResponse, LoginListResponse, LocoPacket, SyncState } from './types'
19
20
 
20
21
  // LOCO opcode string emitted on the wire for typing indicator pulses.
@@ -81,27 +82,34 @@ export class LocoSession {
81
82
  const lastTokenId = syncState ? new Long(syncState.lastTokenId.low, syncState.lastTokenId.high) : Long.fromNumber(0)
82
83
  const lbk = syncState?.lbk ?? 0
83
84
 
84
- const response = await this.connection.sendPacket('LOGINLIST', {
85
- appVer: deviceConfig.appVersion,
86
- prtVer: PROTOCOL_VERSION,
87
- os: deviceConfig.os,
88
- lang: LANG,
89
- dtype: DTYPE,
90
- duuid: deviceUuid,
91
- oauthToken,
92
- ntype: 0,
93
- MCCMNC: MCCMNC,
94
- revision: syncState?.revision ?? 0,
95
- chatIds,
96
- maxIds,
97
- lastTokenId,
98
- lbk,
99
- rp: new Binary(Buffer.from([0x00, 0x00, 0xff, 0xff, 0x00, 0x00])),
100
- bg: false,
101
- })
102
-
103
- this.startPing()
104
- return response.body as unknown as LoginListResponse
85
+ try {
86
+ const response = await this.connection.sendPacket('LOGINLIST', {
87
+ appVer: deviceConfig.appVersion,
88
+ prtVer: PROTOCOL_VERSION,
89
+ os: deviceConfig.os,
90
+ lang: LANG,
91
+ dtype: DTYPE,
92
+ duuid: deviceUuid,
93
+ oauthToken,
94
+ ntype: 0,
95
+ MCCMNC: MCCMNC,
96
+ revision: syncState?.revision ?? 0,
97
+ chatIds,
98
+ maxIds,
99
+ lastTokenId,
100
+ lbk,
101
+ rp: new Binary(Buffer.from([0x00, 0x00, 0xff, 0xff, 0x00, 0x00])),
102
+ bg: false,
103
+ })
104
+
105
+ const loginResult = validateLoginListResponse(response)
106
+ this.startPing()
107
+ return loginResult
108
+ } catch (error) {
109
+ this.connection.close()
110
+ this.connection = null
111
+ throw error
112
+ }
105
113
  }
106
114
 
107
115
  private async bookAndCheckin(
@@ -36,6 +36,7 @@ export interface SyncState {
36
36
 
37
37
  // LOGINLIST uses short BSON field names: c=chatId, t=type, a=activeMembers, etc.
38
38
  export interface LoginListResponse extends ChatListResponse {
39
+ status?: number
39
40
  userId: number
40
41
  revision: number
41
42
  }
@@ -12,6 +12,7 @@ export interface ExtractedKakaoToken {
12
12
  }
13
13
 
14
14
  export type KakaoAuthMethod = 'login' | 'extract'
15
+ export type KakaoAuthErrorCode = 'invalid_access_token' | 'login_rejected'
15
16
 
16
17
  export interface KakaoAccountCredentials {
17
18
  account_id: string