agent-messenger 2.26.0 → 2.27.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 (43) hide show
  1. package/.claude-plugin/README.md +16 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +27 -0
  4. package/dist/package.json +1 -1
  5. package/dist/src/platforms/discord/listener.d.ts +9 -4
  6. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  7. package/dist/src/platforms/discord/listener.js +110 -16
  8. package/dist/src/platforms/discord/listener.js.map +1 -1
  9. package/dist/src/platforms/discordbot/listener.d.ts +6 -0
  10. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
  11. package/dist/src/platforms/discordbot/listener.js +78 -2
  12. package/dist/src/platforms/discordbot/listener.js.map +1 -1
  13. package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -1
  14. package/dist/src/platforms/slack/qr-http-login.js +105 -6
  15. package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
  16. package/docs/content/docs/sdk/discord.mdx +1 -9
  17. package/e2e/README.md +3 -1
  18. package/package.json +1 -1
  19. package/skills/agent-channeltalk/SKILL.md +1 -1
  20. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  21. package/skills/agent-discord/SKILL.md +37 -8
  22. package/skills/agent-discord/references/authentication.md +46 -8
  23. package/skills/agent-discordbot/SKILL.md +1 -1
  24. package/skills/agent-instagram/SKILL.md +1 -1
  25. package/skills/agent-kakaotalk/SKILL.md +1 -1
  26. package/skills/agent-line/SKILL.md +1 -1
  27. package/skills/agent-slack/SKILL.md +7 -4
  28. package/skills/agent-slack/references/authentication.md +8 -3
  29. package/skills/agent-slackbot/SKILL.md +1 -1
  30. package/skills/agent-teams/SKILL.md +1 -1
  31. package/skills/agent-telegram/SKILL.md +1 -1
  32. package/skills/agent-telegrambot/SKILL.md +1 -1
  33. package/skills/agent-webex/SKILL.md +1 -1
  34. package/skills/agent-webexbot/SKILL.md +1 -1
  35. package/skills/agent-wechatbot/SKILL.md +1 -1
  36. package/skills/agent-whatsapp/SKILL.md +1 -1
  37. package/skills/agent-whatsappbot/SKILL.md +1 -1
  38. package/src/platforms/discord/listener.test.ts +187 -147
  39. package/src/platforms/discord/listener.ts +122 -18
  40. package/src/platforms/discordbot/listener.test.ts +186 -184
  41. package/src/platforms/discordbot/listener.ts +83 -2
  42. package/src/platforms/slack/qr-http-login.test.ts +47 -0
  43. package/src/platforms/slack/qr-http-login.ts +112 -7
@@ -4,7 +4,7 @@ import WebSocket from 'ws'
4
4
 
5
5
  import type { DiscordClient } from './client'
6
6
  import type { DiscordListenerEventMap, DiscordGatewayGenericEvent } from './types'
7
- import { DiscordGatewayOpcode, DiscordIntent } from './types'
7
+ import { DiscordGatewayOpcode } from './types'
8
8
 
9
9
  const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json'
10
10
  const GATEWAY_QUERY = '?v=10&encoding=json'
@@ -13,20 +13,35 @@ const RECONNECT_MAX_DELAY = 30_000
13
13
  const NON_RECOVERABLE_CLOSE_CODES = [4004, 4010, 4011, 4012, 4013, 4014]
14
14
  const SESSION_RESET_CLOSE_CODES = [4007, 4009]
15
15
 
16
- const DEFAULT_INTENTS =
17
- DiscordIntent.Guilds |
18
- DiscordIntent.GuildMessages |
19
- DiscordIntent.GuildMessageReactions |
20
- DiscordIntent.GuildMessageTyping |
21
- DiscordIntent.DirectMessages |
22
- DiscordIntent.DirectMessageReactions |
23
- DiscordIntent.DirectMessageTyping
16
+ // Hello arrives within milliseconds and READY within a few seconds on a healthy gateway;
17
+ // 15s is generous for a successful handshake while still failing fast on bad tokens,
18
+ // network stalls, or a gateway that accepts the socket but never delivers READY.
19
+ const DEFAULT_CONNECT_TIMEOUT_MS = 15_000
20
+
21
+ // User (non-bot) gateway capabilities bitmask, mirroring discord.py-self's default set.
22
+ // Capabilities shape the READY payload; bit 10 (client_state_v2) requires client_state.guild_versions.
23
+ const USER_GATEWAY_CAPABILITIES = 16381
24
+
25
+ // Without MESSAGE_CONTENT (1<<15), Discord blanks `content`/`embeds`/`attachments` on messages
26
+ // from OTHER users (self/DM/mention content still arrives). User sessions get all intents only
27
+ // when `intents` is omitted OR explicitly set, so we send an all-intents value to guarantee content.
28
+ const USER_GATEWAY_INTENTS = 33_554_431
29
+
30
+ // Discord validates client_build_number against recent web-client builds; a stale value can
31
+ // yield a connected-but-degraded session that fires READY yet delivers no message events.
32
+ // Overridable via env so it can be refreshed without a release when it eventually goes stale.
33
+ const DEFAULT_CLIENT_BUILD_NUMBER = 648814
34
+ const USER_GATEWAY_BUILD_NUMBER = Number(process.env.AGENT_DISCORD_BUILD_NUMBER) || DEFAULT_CLIENT_BUILD_NUMBER
24
35
 
25
36
  type EventKey = keyof DiscordListenerEventMap
26
37
 
38
+ export interface DiscordListenerOptions {
39
+ connectTimeoutMs?: number
40
+ }
41
+
27
42
  export class DiscordListener {
28
43
  private client: DiscordClient
29
- private intents: number
44
+ private connectTimeoutMs: number
30
45
  private running = false
31
46
  private ws: WebSocket | null = null
32
47
  private emitter = new EventEmitter()
@@ -35,6 +50,7 @@ export class DiscordListener {
35
50
  private heartbeatJitterTimer: ReturnType<typeof setTimeout> | null = null
36
51
  private invalidSessionTimer: ReturnType<typeof setTimeout> | null = null
37
52
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null
53
+ private connectTimeoutTimer: ReturnType<typeof setTimeout> | null = null
38
54
  private reconnectAttempts = 0
39
55
  private sequence: number | null = null
40
56
  private sessionId: string | null = null
@@ -42,24 +58,90 @@ export class DiscordListener {
42
58
  private token: string | null = null
43
59
  private cachedUser: { id: string; username: string } | null = null
44
60
  private generation = 0
61
+ private startPromise: Promise<void> | null = null
62
+ private pendingStartReject: ((error: Error) => void) | null = null
45
63
 
46
- constructor(client: DiscordClient, options?: { intents?: number }) {
64
+ constructor(client: DiscordClient, options?: DiscordListenerOptions) {
47
65
  this.client = client
48
- this.intents = options?.intents ?? DEFAULT_INTENTS
66
+ this.connectTimeoutMs = options?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
49
67
  }
50
68
 
51
69
  async start(): Promise<void> {
70
+ if (this.startPromise) return this.startPromise
52
71
  if (this.running) return
72
+
53
73
  this.running = true
54
74
  this.reconnectAttempts = 0
55
- this.generation++
56
- await this.connect(this.generation)
75
+ const generation = ++this.generation
76
+
77
+ const ready = new Promise<void>((resolve, reject) => {
78
+ let settled = false
79
+
80
+ const cleanup = () => {
81
+ this.emitter.off('connected', onConnected)
82
+ this.emitter.off('error', onError)
83
+ this.pendingStartReject = null
84
+ if (this.connectTimeoutTimer) {
85
+ clearTimeout(this.connectTimeoutTimer)
86
+ this.connectTimeoutTimer = null
87
+ }
88
+ }
89
+
90
+ const onConnected = () => {
91
+ if (settled || !this.isCurrent(generation)) return
92
+ settled = true
93
+ cleanup()
94
+ resolve()
95
+ }
96
+
97
+ const onError = (error: Error) => {
98
+ if (settled || !this.isCurrent(generation)) return
99
+ settled = true
100
+ cleanup()
101
+ this.teardownFailedStart(generation)
102
+ reject(error)
103
+ }
104
+
105
+ this.emitter.once('connected', onConnected)
106
+ this.emitter.once('error', onError)
107
+
108
+ // Generation-agnostic on purpose: stop() invokes this to reject an in-flight start
109
+ // (after bumping generation) without leaking the once() handlers.
110
+ this.pendingStartReject = (error: Error) => {
111
+ if (settled) return
112
+ settled = true
113
+ cleanup()
114
+ reject(error)
115
+ }
116
+
117
+ this.connectTimeoutTimer = setTimeout(() => {
118
+ onError(new Error(`Discord gateway did not become ready within ${this.connectTimeoutMs}ms`))
119
+ }, this.connectTimeoutMs)
120
+ })
121
+
122
+ const run = async (): Promise<void> => {
123
+ try {
124
+ await Promise.all([this.connect(generation), ready])
125
+ } finally {
126
+ if (this.generation === generation) this.startPromise = null
127
+ }
128
+ }
129
+ const startPromise = run()
130
+ this.startPromise = startPromise
131
+ return startPromise
57
132
  }
58
133
 
59
134
  stop(): void {
60
135
  this.running = false
61
136
  this.generation++
62
137
  this.clearTimers()
138
+ if (this.connectTimeoutTimer) {
139
+ clearTimeout(this.connectTimeoutTimer)
140
+ this.connectTimeoutTimer = null
141
+ }
142
+ const rejectStart = this.pendingStartReject
143
+ this.pendingStartReject = null
144
+ this.startPromise = null
63
145
  if (this.ws) {
64
146
  this.ws.close()
65
147
  this.ws = null
@@ -69,6 +151,12 @@ export class DiscordListener {
69
151
  this.resumeGatewayUrl = null
70
152
  this.token = null
71
153
  this.cachedUser = null
154
+ rejectStart?.(new Error('Discord gateway start was stopped before becoming ready'))
155
+ }
156
+
157
+ private teardownFailedStart(generation: number): void {
158
+ if (!this.isCurrent(generation)) return
159
+ this.stop()
72
160
  }
73
161
 
74
162
  on<K extends EventKey>(event: K, listener: (...args: DiscordListenerEventMap[K]) => void): this {
@@ -233,12 +321,28 @@ export class DiscordListener {
233
321
  op: DiscordGatewayOpcode.Identify,
234
322
  d: {
235
323
  token: this.token,
236
- intents: this.intents,
324
+ capabilities: USER_GATEWAY_CAPABILITIES,
325
+ intents: USER_GATEWAY_INTENTS,
237
326
  properties: {
238
- os: 'linux',
239
- browser: 'agent-messenger',
240
- device: 'agent-messenger',
327
+ os: 'Linux',
328
+ browser: 'Chrome',
329
+ device: '',
330
+ system_locale: 'en-US',
331
+ browser_user_agent:
332
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
333
+ browser_version: '131.0.0.0',
334
+ os_version: '',
335
+ referrer: '',
336
+ referring_domain: '',
337
+ referrer_current: '',
338
+ referring_domain_current: '',
339
+ release_channel: 'stable',
340
+ client_build_number: USER_GATEWAY_BUILD_NUMBER,
341
+ client_event_source: null,
241
342
  },
343
+ presence: { status: 'online', since: 0, activities: [], afk: false },
344
+ compress: false,
345
+ client_state: { guild_versions: {} },
242
346
  },
243
347
  }),
244
348
  )