agent-messenger 2.32.0 → 2.32.2

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 (72) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bun.lock +7 -0
  3. package/dist/package.json +2 -1
  4. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  5. package/dist/src/platforms/channeltalk/token-extractor.js +67 -18
  6. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  7. package/dist/src/platforms/discord/commands/channel.d.ts.map +1 -1
  8. package/dist/src/platforms/discord/commands/channel.js +3 -2
  9. package/dist/src/platforms/discord/commands/channel.js.map +1 -1
  10. package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
  11. package/dist/src/platforms/discord/commands/snapshot.js +7 -6
  12. package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
  13. package/dist/src/platforms/discord/types.d.ts +17 -0
  14. package/dist/src/platforms/discord/types.d.ts.map +1 -1
  15. package/dist/src/platforms/discord/types.js +36 -0
  16. package/dist/src/platforms/discord/types.js.map +1 -1
  17. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  18. package/dist/src/platforms/kakaotalk/client.js +2 -0
  19. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  20. package/dist/src/platforms/slack/index.d.ts +1 -0
  21. package/dist/src/platforms/slack/index.d.ts.map +1 -1
  22. package/dist/src/platforms/slack/index.js.map +1 -1
  23. package/dist/src/platforms/slack/qr-confirmation.d.ts +19 -0
  24. package/dist/src/platforms/slack/qr-confirmation.d.ts.map +1 -0
  25. package/dist/src/platforms/slack/qr-confirmation.js +68 -0
  26. package/dist/src/platforms/slack/qr-confirmation.js.map +1 -0
  27. package/dist/src/platforms/slack/qr-cookie-jar.d.ts +7 -0
  28. package/dist/src/platforms/slack/qr-cookie-jar.d.ts.map +1 -0
  29. package/dist/src/platforms/slack/qr-cookie-jar.js +48 -0
  30. package/dist/src/platforms/slack/qr-cookie-jar.js.map +1 -0
  31. package/dist/src/platforms/slack/qr-http-login.d.ts +2 -1
  32. package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -1
  33. package/dist/src/platforms/slack/qr-http-login.js +88 -53
  34. package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
  35. package/docs/content/docs/cli/channeltalk.mdx +4 -4
  36. package/package.json +2 -1
  37. package/skills/agent-channeltalk/SKILL.md +5 -5
  38. package/skills/agent-channeltalk/references/authentication.md +9 -5
  39. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  40. package/skills/agent-discord/SKILL.md +8 -4
  41. package/skills/agent-discord/references/common-patterns.md +7 -3
  42. package/skills/agent-discordbot/SKILL.md +1 -1
  43. package/skills/agent-imessage/SKILL.md +1 -1
  44. package/skills/agent-instagram/SKILL.md +1 -1
  45. package/skills/agent-kakaotalk/SKILL.md +1 -1
  46. package/skills/agent-line/SKILL.md +1 -1
  47. package/skills/agent-slack/SKILL.md +25 -2
  48. package/skills/agent-slackbot/SKILL.md +1 -1
  49. package/skills/agent-teams/SKILL.md +1 -1
  50. package/skills/agent-telegram/SKILL.md +1 -1
  51. package/skills/agent-telegrambot/SKILL.md +1 -1
  52. package/skills/agent-webex/SKILL.md +1 -1
  53. package/skills/agent-webexbot/SKILL.md +1 -1
  54. package/skills/agent-wechatbot/SKILL.md +1 -1
  55. package/skills/agent-whatsapp/SKILL.md +1 -1
  56. package/skills/agent-whatsappbot/SKILL.md +1 -1
  57. package/src/platforms/channeltalk/token-extractor.test.ts +235 -0
  58. package/src/platforms/channeltalk/token-extractor.ts +81 -22
  59. package/src/platforms/discord/commands/channel.test.ts +33 -23
  60. package/src/platforms/discord/commands/channel.ts +3 -2
  61. package/src/platforms/discord/commands/snapshot.test.ts +80 -2
  62. package/src/platforms/discord/commands/snapshot.ts +7 -6
  63. package/src/platforms/discord/types.test.ts +45 -0
  64. package/src/platforms/discord/types.ts +41 -0
  65. package/src/platforms/kakaotalk/client.test.ts +29 -0
  66. package/src/platforms/kakaotalk/client.ts +1 -0
  67. package/src/platforms/slack/index.ts +1 -0
  68. package/src/platforms/slack/qr-confirmation.ts +86 -0
  69. package/src/platforms/slack/qr-cookie-jar.ts +54 -0
  70. package/src/platforms/slack/qr-http-login-2fa.test.ts +398 -0
  71. package/src/platforms/slack/qr-http-login.test.ts +10 -18
  72. package/src/platforms/slack/qr-http-login.ts +109 -52
@@ -2,6 +2,7 @@ import { expect, it } from 'bun:test'
2
2
 
3
3
  import {
4
4
  DiscordChannelSchema,
5
+ DiscordChannelType,
5
6
  DiscordConfigSchema,
6
7
  DiscordCredentialsSchema,
7
8
  DiscordError,
@@ -10,6 +11,8 @@ import {
10
11
  DiscordMessageSchema,
11
12
  DiscordReactionSchema,
12
13
  DiscordUserSchema,
14
+ isListableChannel,
15
+ isMessageReadableChannel,
13
16
  } from './types'
14
17
 
15
18
  it('DiscordGuildSchema validates correct guild', () => {
@@ -255,3 +258,45 @@ it('DiscordError is instance of Error', () => {
255
258
  const error = new DiscordError('Test error', 'TEST_CODE')
256
259
  expect(error instanceof Error).toBe(true)
257
260
  })
261
+
262
+ it('isListableChannel includes text, announcement, voice, stage, forum, media, and directory', () => {
263
+ const listable = [
264
+ DiscordChannelType.GUILD_TEXT,
265
+ DiscordChannelType.GUILD_ANNOUNCEMENT,
266
+ DiscordChannelType.GUILD_VOICE,
267
+ DiscordChannelType.GUILD_STAGE_VOICE,
268
+ DiscordChannelType.GUILD_FORUM,
269
+ DiscordChannelType.GUILD_MEDIA,
270
+ DiscordChannelType.GUILD_DIRECTORY,
271
+ ]
272
+ expect(listable.every((type) => isListableChannel({ type }))).toBe(true)
273
+ })
274
+
275
+ it('isListableChannel excludes categories and threads', () => {
276
+ const excluded = [
277
+ DiscordChannelType.GUILD_CATEGORY,
278
+ DiscordChannelType.ANNOUNCEMENT_THREAD,
279
+ DiscordChannelType.PUBLIC_THREAD,
280
+ DiscordChannelType.PRIVATE_THREAD,
281
+ ]
282
+ expect(excluded.some((type) => isListableChannel({ type }))).toBe(false)
283
+ })
284
+
285
+ it('isMessageReadableChannel includes text, announcement, voice, and stage', () => {
286
+ const readable = [
287
+ DiscordChannelType.GUILD_TEXT,
288
+ DiscordChannelType.GUILD_ANNOUNCEMENT,
289
+ DiscordChannelType.GUILD_VOICE,
290
+ DiscordChannelType.GUILD_STAGE_VOICE,
291
+ ]
292
+ expect(readable.every((type) => isMessageReadableChannel({ type }))).toBe(true)
293
+ })
294
+
295
+ it('isMessageReadableChannel excludes forum, media, and directory containers', () => {
296
+ const notReadable = [
297
+ DiscordChannelType.GUILD_FORUM,
298
+ DiscordChannelType.GUILD_MEDIA,
299
+ DiscordChannelType.GUILD_DIRECTORY,
300
+ ]
301
+ expect(notReadable.some((type) => isMessageReadableChannel({ type }))).toBe(false)
302
+ })
@@ -27,6 +27,47 @@ export interface DiscordChannel {
27
27
  }
28
28
  }
29
29
 
30
+ // https://discord.com/developers/docs/resources/channel#channel-object-channel-types
31
+ export const DiscordChannelType = {
32
+ GUILD_TEXT: 0,
33
+ DM: 1,
34
+ GUILD_VOICE: 2,
35
+ GROUP_DM: 3,
36
+ GUILD_CATEGORY: 4,
37
+ GUILD_ANNOUNCEMENT: 5,
38
+ ANNOUNCEMENT_THREAD: 10,
39
+ PUBLIC_THREAD: 11,
40
+ PRIVATE_THREAD: 12,
41
+ GUILD_STAGE_VOICE: 13,
42
+ GUILD_DIRECTORY: 14,
43
+ GUILD_FORUM: 15,
44
+ GUILD_MEDIA: 16,
45
+ } as const
46
+
47
+ const THREAD_CHANNEL_TYPES: ReadonlySet<number> = new Set([
48
+ DiscordChannelType.ANNOUNCEMENT_THREAD,
49
+ DiscordChannelType.PUBLIC_THREAD,
50
+ DiscordChannelType.PRIVATE_THREAD,
51
+ ])
52
+
53
+ // Channels with no directly fetchable message timeline:
54
+ // forum/media messages live in child posts/threads, and directory channels
55
+ // are hub listings, not message channels. Fetching messages from these aborts.
56
+ const NON_MESSAGE_CHANNEL_TYPES: ReadonlySet<number> = new Set([
57
+ DiscordChannelType.GUILD_FORUM,
58
+ DiscordChannelType.GUILD_MEDIA,
59
+ DiscordChannelType.GUILD_DIRECTORY,
60
+ ])
61
+
62
+ // Voice/stage channels are listed too because they carry embedded text chat.
63
+ export function isListableChannel(channel: Pick<DiscordChannel, 'type'>): boolean {
64
+ return channel.type !== DiscordChannelType.GUILD_CATEGORY && !THREAD_CHANNEL_TYPES.has(channel.type)
65
+ }
66
+
67
+ export function isMessageReadableChannel(channel: Pick<DiscordChannel, 'type'>): boolean {
68
+ return isListableChannel(channel) && !NON_MESSAGE_CHANNEL_TYPES.has(channel.type)
69
+ }
70
+
30
71
  export interface DiscordMessage {
31
72
  id: string
32
73
  channel_id: string
@@ -107,6 +107,7 @@ describe('KakaoTalkClient', () => {
107
107
  mockLogin.mockResolvedValue(structuredClone(DEFAULT_LOGIN_RESULT))
108
108
  mockGetChatLogs.mockResolvedValue({ body: { status: 0, chatLogs: [], eof: true } })
109
109
  mockGetChatInfo.mockResolvedValue({ body: { l: makeLong(99999) } })
110
+ mockSyncMessages.mockResolvedValue({ body: { status: 0, isOK: true, chatLogs: [] } })
110
111
  })
111
112
 
112
113
  afterEach(() => {
@@ -803,6 +804,34 @@ describe('KakaoTalkClient', () => {
803
804
  })
804
805
 
805
806
  describe('getMessages', () => {
807
+ it('falls back to SYNCMSG when MCHATLOGS succeeds with an empty result', async () => {
808
+ mockGetChatLogs.mockResolvedValueOnce({
809
+ body: { status: 0, chatLogs: [], eof: true },
810
+ })
811
+ mockGetChatInfo.mockResolvedValueOnce({
812
+ body: { status: 0, l: makeLong(10) },
813
+ })
814
+ mockSyncMessages.mockResolvedValueOnce({
815
+ body: {
816
+ status: 0,
817
+ isOK: true,
818
+ chatLogs: [
819
+ { logId: makeLong(10), chatId: 100, type: 1, authorId: 42, message: 'fallback', sendAt: 1700000001 },
820
+ ],
821
+ },
822
+ })
823
+
824
+ const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
825
+ const messages = await client.getMessages('100')
826
+
827
+ expect(messages).toHaveLength(1)
828
+ expect(messages[0]?.message).toBe('fallback')
829
+ expect(mockGetChatInfo).toHaveBeenCalledTimes(1)
830
+ expect(mockSyncMessages).toHaveBeenCalledTimes(1)
831
+
832
+ client.close()
833
+ })
834
+
806
835
  it('returns formatted messages', async () => {
807
836
  mockGetChatLogs.mockResolvedValueOnce({
808
837
  body: {
@@ -850,6 +850,7 @@ export class KakaoTalkClient {
850
850
  (log) => longToString(log.chatId) === chatId,
851
851
  )
852
852
  if (batch.length === 0) {
853
+ if (allMessages.length === 0) break
853
854
  return formatMessages(allMessages, count, chatId, this.nameCache)
854
855
  }
855
856
 
@@ -3,6 +3,7 @@ export { SlackCredentialManager, CredentialManager } from './credential-manager'
3
3
  export { SlackListener } from './listener'
4
4
  export { loginWithQr } from './qr-http-login'
5
5
  export type { QrLoginOptions, QrSession } from './qr-http-login'
6
+ export type { SlackConfirmationChallenge, SlackConfirmationCodeRequest } from './qr-confirmation'
6
7
  export { decodeSlackQr } from './qr-login'
7
8
  export type { SlackQrLogin } from './qr-login'
8
9
  export type {
@@ -0,0 +1,86 @@
1
+ import type { SlackQrLogin } from './qr-login'
2
+
3
+ export interface SlackConfirmationChallenge {
4
+ readonly email: string
5
+ readonly type: string | null
6
+ }
7
+
8
+ export type SlackConfirmationCodeRequest = (challenge: SlackConfirmationChallenge) => Promise<string>
9
+
10
+ type SigninProps = {
11
+ readonly magicLogin: string
12
+ readonly emailAddress: string
13
+ readonly twoFactorType: string | null
14
+ }
15
+
16
+ export function parseConfirmationPage(html: string): SigninProps | null {
17
+ const match = html.match(/id="props_node"[^>]*\bdata-props="([^"]*)"/i)
18
+ const encoded = match?.[1]
19
+ if (!encoded) return null
20
+
21
+ let parsed: unknown
22
+ try {
23
+ parsed = JSON.parse(decodeHtml(encoded))
24
+ } catch {
25
+ return null
26
+ }
27
+ if (!isRecord(parsed)) return null
28
+
29
+ const magicLogin = parsed.magicLogin
30
+ const emailAddress = parsed.emailAddress
31
+ if (typeof magicLogin !== 'string' || magicLogin === '') return null
32
+ if (typeof emailAddress !== 'string' || emailAddress === '') return null
33
+ return {
34
+ magicLogin,
35
+ emailAddress,
36
+ twoFactorType: typeof parsed.twoFactorType === 'string' ? parsed.twoFactorType : null,
37
+ }
38
+ }
39
+
40
+ export function buildConfirmationRequest(
41
+ login: SlackQrLogin,
42
+ finalUrl: string,
43
+ props: SigninProps,
44
+ code: string,
45
+ ): { readonly url: string; readonly body: string } | null {
46
+ const expected = expectedConfirmationPath(login)
47
+ if (!expected) return null
48
+ const url = new URL(finalUrl)
49
+ if (url.hostname !== `${login.workspace}.slack.com`) return null
50
+ if (url.pathname !== expected) return null
51
+
52
+ url.search = ''
53
+ url.searchParams.set('domain', login.workspace)
54
+ url.searchParams.set('domainLogin', '1')
55
+ url.searchParams.set('email', props.emailAddress)
56
+
57
+ return {
58
+ url: url.toString(),
59
+ body: new URLSearchParams({
60
+ '2fa_magiclogin': props.magicLogin,
61
+ remember: '0',
62
+ has_remember: 'false',
63
+ '2fa_code': code.trim(),
64
+ '2fa_action': 'submit_primary',
65
+ }).toString(),
66
+ }
67
+ }
68
+
69
+ export function expectedConfirmationPath(login: SlackQrLogin): string | null {
70
+ const qrSecret = new URL(login.url).pathname.split('/').at(-1)
71
+ if (!qrSecret || !qrSecret.startsWith('z-app-')) return null
72
+ return `/login/${qrSecret}`
73
+ }
74
+
75
+ function decodeHtml(value: string): string {
76
+ return value
77
+ .replaceAll('&quot;', '"')
78
+ .replaceAll('&#39;', "'")
79
+ .replaceAll('&lt;', '<')
80
+ .replaceAll('&gt;', '>')
81
+ .replaceAll('&amp;', '&')
82
+ }
83
+
84
+ function isRecord(value: unknown): value is Record<string, unknown> {
85
+ return typeof value === 'object' && value !== null
86
+ }
@@ -0,0 +1,54 @@
1
+ import { CookieJar } from 'tough-cookie'
2
+
3
+ const BROWSER_USER_AGENT =
4
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'
5
+
6
+ const SESSION_COOKIE_NAME = 'd'
7
+ const SESSION_COOKIE_PREFIX = 'xoxd-'
8
+
9
+ export class SlackCookieJar {
10
+ readonly #jar = new CookieJar()
11
+
12
+ async fetch(doFetch: typeof fetch, url: string, init: RequestInit = {}): Promise<Response> {
13
+ const cookie = await this.#jar.getCookieString(url)
14
+ const response = await doFetch(url, {
15
+ ...init,
16
+ redirect: 'manual',
17
+ headers: {
18
+ 'User-Agent': BROWSER_USER_AGENT,
19
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
20
+ 'Accept-Language': 'en-US,en;q=0.9',
21
+ ...init.headers,
22
+ ...(cookie ? { Cookie: cookie } : {}),
23
+ },
24
+ })
25
+ for (const setCookie of getSetCookies(response)) {
26
+ await this.#jar.setCookie(setCookie, url, { ignoreError: true })
27
+ }
28
+ return response
29
+ }
30
+
31
+ async sessionCookie(url: string): Promise<string | null> {
32
+ const cookies = await this.#jar.getCookies(url)
33
+ for (const cookie of cookies) {
34
+ if (cookie.key === SESSION_COOKIE_NAME && cookie.value.startsWith(SESSION_COOKIE_PREFIX)) {
35
+ return cookie.value
36
+ }
37
+ }
38
+ return null
39
+ }
40
+ }
41
+
42
+ export function setCookieNames(response: Response): readonly string[] {
43
+ return getSetCookies(response).flatMap((setCookie) => {
44
+ const name = setCookie.split('=')[0]?.trim()
45
+ return name ? [name] : []
46
+ })
47
+ }
48
+
49
+ function getSetCookies(response: Response): readonly string[] {
50
+ const withGetter = response.headers as Headers & { getSetCookie?: () => string[] }
51
+ if (typeof withGetter.getSetCookie === 'function') return withGetter.getSetCookie()
52
+ const single = response.headers.get('set-cookie')
53
+ return single ? [single] : []
54
+ }