@hybrd/xmtp 1.3.2 → 1.4.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 (38) hide show
  1. package/README.md +41 -7
  2. package/dist/index.cjs +415 -3085
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +12 -828
  5. package/dist/index.d.ts +12 -828
  6. package/dist/index.js +416 -3056
  7. package/dist/index.js.map +1 -1
  8. package/package.json +18 -5
  9. package/src/client.ts +23 -135
  10. package/src/index.ts +28 -81
  11. package/src/index.ts.old +145 -0
  12. package/src/lib/jwt.ts +45 -13
  13. package/src/lib/{message-listener.test.ts → message-listener.test.ts.old} +1 -1
  14. package/src/lib/subjects.ts +6 -5
  15. package/src/plugin.filters.test.ts +158 -0
  16. package/src/plugin.ts +456 -23
  17. package/src/resolver/address-resolver.ts +217 -211
  18. package/src/resolver/basename-resolver.ts +6 -5
  19. package/src/resolver/ens-resolver.ts +15 -14
  20. package/src/resolver/resolver.ts +3 -2
  21. package/src/resolver/xmtp-resolver.ts +10 -9
  22. package/src/{service-client.ts → service-client.ts.old} +26 -3
  23. package/src/types.ts +9 -157
  24. package/src/types.ts.old +157 -0
  25. package/.cache/tsbuildinfo.json +0 -1
  26. package/.turbo/turbo-build.log +0 -45
  27. package/.turbo/turbo-lint$colon$fix.log +0 -6
  28. package/.turbo/turbo-typecheck.log +0 -5
  29. package/biome.jsonc +0 -4
  30. package/scripts/generate-keys.ts +0 -25
  31. package/scripts/refresh-identity.ts +0 -119
  32. package/scripts/register-wallet.ts +0 -95
  33. package/scripts/revoke-all-installations.ts +0 -91
  34. package/scripts/revoke-installations.ts +0 -94
  35. package/src/endpoints.ts +0 -306
  36. package/tsconfig.json +0 -9
  37. package/tsup.config.ts +0 -14
  38. /package/src/lib/{message-listener.ts → message-listener.ts.old} +0 -0
package/src/plugin.ts CHANGED
@@ -1,19 +1,65 @@
1
- import { Hono } from "hono"
2
- import xmtpEndpoints from "./endpoints"
3
- import type { MessageListenerConfig } from "./lib/message-listener"
4
- import type { HonoVariables } from "./types"
5
-
6
- export interface Plugin<TContext = unknown> {
7
- name: string
8
- description?: string
9
- apply: (
10
- app: Hono<{ Variables: HonoVariables }>,
11
- context?: TContext
12
- ) => void | Promise<void>
13
- }
1
+ import {
2
+ Agent as XmtpAgent,
3
+ XmtpEnv,
4
+ createSigner,
5
+ createUser
6
+ } from "@xmtp/agent-sdk"
7
+
8
+ import type {
9
+ AgentMessage,
10
+ AgentRuntime,
11
+ BehaviorContext,
12
+ BehaviorRegistry,
13
+ Plugin,
14
+ PluginContext,
15
+ XmtpClient,
16
+ XmtpConversation,
17
+ XmtpMessage
18
+ } from "@hybrd/types"
19
+ import { logger } from "@hybrd/utils"
20
+ import { randomUUID } from "node:crypto"
21
+ import { createXMTPClient, getDbPath } from "./client"
22
+ import { ContentTypeReply, ContentTypeText, type Reply } from "./index"
23
+
24
+ // Re-export types from @hybrd/types for backward compatibility
25
+ export type { Plugin }
26
+
27
+ /**
28
+ * Send a response with threading support
29
+ */
30
+ async function sendResponse(
31
+ conversation: XmtpConversation,
32
+ text: string,
33
+ originalMessageId: string,
34
+ behaviorContext?: BehaviorContext
35
+ ) {
36
+ const shouldThread = behaviorContext?.sendOptions?.threaded ?? false
14
37
 
15
- export interface XMTPPluginContext {
16
- agent: unknown
38
+ if (shouldThread) {
39
+ // Send as a reply to the original message
40
+ try {
41
+ const reply: Reply = {
42
+ reference: originalMessageId,
43
+ contentType: ContentTypeText,
44
+ content: text
45
+ }
46
+ await conversation.send(reply, ContentTypeReply)
47
+ logger.debug(
48
+ `✅ [sendResponse] Threaded reply sent successfully to message ${originalMessageId}`
49
+ )
50
+ } catch (error) {
51
+ logger.error(
52
+ `❌ [sendResponse] Failed to send threaded reply to message ${originalMessageId}:`,
53
+ error
54
+ )
55
+ // Fall back to regular message if threaded reply fails
56
+ logger.debug(`🔄 [sendResponse] Falling back to regular message`)
57
+ await conversation.send(text)
58
+ }
59
+ } else {
60
+ // Send as a regular message
61
+ await conversation.send(text)
62
+ }
17
63
  }
18
64
 
19
65
  /**
@@ -23,17 +69,404 @@ export interface XMTPPluginContext {
23
69
  * This plugin integrates XMTP messaging capabilities into the agent's
24
70
  * HTTP server. It mounts the XMTP endpoints for handling XMTP tools requests.
25
71
  */
26
- export function XMTPPlugin({
27
- filter
28
- }: {
29
- filter?: MessageListenerConfig["filter"]
30
- } = {}): Plugin<XMTPPluginContext> {
72
+ export function XMTPPlugin(): Plugin<PluginContext> {
31
73
  return {
32
74
  name: "xmtp",
33
75
  description: "Provides XMTP messaging functionality",
34
- apply: (app, context) => {
35
- // Mount the XMTP endpoints at /xmtp-tools
36
- app.route("/xmtp-tools", xmtpEndpoints)
76
+ apply: async (app, context): Promise<void> => {
77
+ const {
78
+ XMTP_WALLET_KEY,
79
+ XMTP_DB_ENCRYPTION_KEY,
80
+ XMTP_ENV = "production"
81
+ } = process.env
82
+
83
+ const { agent } = context
84
+ const pluginContext = context as PluginContext & {
85
+ behaviors?: BehaviorRegistry
86
+ }
87
+
88
+ if (!XMTP_WALLET_KEY) {
89
+ throw new Error("XMTP_WALLET_KEY must be set")
90
+ }
91
+
92
+ if (!XMTP_DB_ENCRYPTION_KEY) {
93
+ throw new Error("XMTP_DB_ENCRYPTION_KEY must be set")
94
+ }
95
+
96
+ const user = createUser(XMTP_WALLET_KEY as `0x${string}`)
97
+ const signer = createSigner(user)
98
+
99
+ const xmtpClient = await createXMTPClient(
100
+ XMTP_WALLET_KEY as `0x${string}`
101
+ )
102
+
103
+ // Start a reliable node client stream to process incoming messages
104
+ async function startNodeStream() {
105
+ try {
106
+ logger.debug("🎧 XMTP node client stream initializing")
107
+ const stream = await xmtpClient.conversations.streamAllMessages()
108
+ logger.debug("🎧 XMTP node client stream started")
109
+ for await (const msg of stream) {
110
+ try {
111
+ if (msg.senderInboxId === xmtpClient.inboxId) continue
112
+
113
+ const content =
114
+ typeof msg.content === "string"
115
+ ? msg.content
116
+ : (() => {
117
+ try {
118
+ return JSON.stringify(msg.content)
119
+ } catch {
120
+ return String(msg.content)
121
+ }
122
+ })()
123
+
124
+ const conversation =
125
+ await xmtpClient.conversations.getConversationById(
126
+ msg.conversationId
127
+ )
128
+
129
+ if (!conversation) {
130
+ logger.warn(
131
+ `⚠️ XMTP conversation not found: ${msg.conversationId}`
132
+ )
133
+ continue
134
+ }
135
+
136
+ const messages: AgentMessage[] = [
137
+ {
138
+ id: randomUUID(),
139
+ role: "user",
140
+ parts: [{ type: "text", text: content }]
141
+ }
142
+ ]
143
+
144
+ const baseRuntime: AgentRuntime = {
145
+ conversation: conversation as unknown as XmtpConversation,
146
+ message: msg,
147
+ xmtpClient
148
+ }
149
+
150
+ const runtime = await agent.createRuntimeContext(baseRuntime)
151
+
152
+ // Execute pre-response behaviors
153
+ if (pluginContext.behaviors) {
154
+ const behaviorContext: BehaviorContext = {
155
+ runtime,
156
+ client: xmtpClient as unknown as XmtpClient,
157
+ conversation: conversation as unknown as XmtpConversation,
158
+ message: msg as XmtpMessage
159
+ }
160
+ await pluginContext.behaviors.executeBefore(behaviorContext)
161
+
162
+ // Check if message was filtered out by any behavior
163
+ if (behaviorContext.sendOptions?.filtered) {
164
+ continue // Skip processing this message
165
+ }
166
+ }
167
+
168
+ const { text } = await agent.generate(messages, { runtime })
169
+
170
+ // Create behavior context for send options
171
+ const behaviorContext: BehaviorContext = {
172
+ runtime,
173
+ client: xmtpClient as unknown as XmtpClient,
174
+ conversation: conversation as unknown as XmtpConversation,
175
+ message: msg as XmtpMessage,
176
+ response: text
177
+ }
178
+
179
+ // Execute post-response behaviors
180
+ if (pluginContext.behaviors) {
181
+ await pluginContext.behaviors.executeAfter(behaviorContext)
182
+ }
183
+
184
+ // Check if message was filtered out by filterMessages behavior
185
+ if (behaviorContext?.sendOptions?.filtered) {
186
+ logger.debug(
187
+ `🔇 [XMTP Plugin] Skipping response due to message being filtered`
188
+ )
189
+ return
190
+ }
191
+
192
+ // Send the response with threading support
193
+ await sendResponse(conversation, text, msg.id, behaviorContext)
194
+ } catch (err) {
195
+ logger.error("❌ Error processing XMTP message:", err)
196
+ }
197
+ }
198
+ } catch (err) {
199
+ logger.error("❌ XMTP node client stream failed:", err)
200
+ }
201
+ }
202
+
203
+ const enabledFromEnv = process.env.XMTP_ENABLE_NODE_STREAM
204
+ const isNodeStreamEnabled =
205
+ enabledFromEnv === undefined
206
+ ? true
207
+ : !/^(false|0|off|no)$/i.test(String(enabledFromEnv))
208
+
209
+ if (isNodeStreamEnabled) void startNodeStream()
210
+
211
+ const address = user.account.address.toLowerCase()
212
+ const agentDbPath = await getDbPath(
213
+ `agent-${XMTP_ENV || "dev"}-${address}`
214
+ )
215
+ logger.debug(`📁 Using agent listener database path: ${agentDbPath}`)
216
+
217
+ const xmtp = await XmtpAgent.create(signer, {
218
+ env: XMTP_ENV as XmtpEnv,
219
+ dbPath: agentDbPath
220
+ })
221
+
222
+ xmtp.on("reaction", async ({ conversation, message }) => {
223
+ try {
224
+ const text = message.content.content
225
+ const messages: AgentMessage[] = [
226
+ {
227
+ id: randomUUID(),
228
+ role: "user",
229
+ parts: [{ type: "text", text }]
230
+ }
231
+ ]
232
+
233
+ const baseRuntime: AgentRuntime = {
234
+ conversation: conversation as unknown as XmtpConversation,
235
+ message: message as unknown as XmtpMessage,
236
+ xmtpClient
237
+ }
238
+
239
+ const runtime = await agent.createRuntimeContext(baseRuntime)
240
+
241
+ // Execute pre-response behaviors
242
+ if (context.behaviors) {
243
+ const behaviorContext: BehaviorContext = {
244
+ runtime,
245
+ client: xmtpClient as unknown as XmtpClient,
246
+ conversation: conversation as unknown as XmtpConversation,
247
+ message: message as unknown as XmtpMessage
248
+ }
249
+ await context.behaviors.executeBefore(behaviorContext)
250
+ }
251
+
252
+ const { text: reply } = await agent.generate(messages, { runtime })
253
+
254
+ // Execute post-response behaviors
255
+ let behaviorContext: BehaviorContext | undefined
256
+ if (context.behaviors) {
257
+ behaviorContext = {
258
+ runtime,
259
+ client: xmtpClient as unknown as XmtpClient,
260
+ conversation: conversation as unknown as XmtpConversation,
261
+ message: message as unknown as XmtpMessage,
262
+ response: reply
263
+ }
264
+ await context.behaviors.executeAfter(behaviorContext)
265
+ } else {
266
+ // Create minimal context for send options
267
+ behaviorContext = {
268
+ runtime,
269
+ client: xmtpClient as unknown as XmtpClient,
270
+ conversation: conversation as unknown as XmtpConversation,
271
+ message: message as unknown as XmtpMessage,
272
+ response: reply
273
+ }
274
+ }
275
+
276
+ // Check if message was filtered out by filterMessages behavior
277
+ if (behaviorContext?.sendOptions?.filtered) {
278
+ logger.debug(
279
+ `🔇 [XMTP Plugin] Skipping reaction response due to message being filtered`
280
+ )
281
+ return
282
+ }
283
+
284
+ await sendResponse(
285
+ conversation as unknown as XmtpConversation,
286
+ reply,
287
+ message.id,
288
+ behaviorContext
289
+ )
290
+ } catch (err) {
291
+ logger.error("❌ Error handling reaction:", err)
292
+ }
293
+ })
294
+
295
+ xmtp.on("reply", async ({ conversation, message }) => {
296
+ try {
297
+ // TODO - why isn't this typed better?
298
+ const text = message.content.content as string
299
+ const messages: AgentMessage[] = [
300
+ {
301
+ id: randomUUID(),
302
+ role: "user",
303
+ parts: [{ type: "text", text }]
304
+ }
305
+ ]
306
+
307
+ const baseRuntime: AgentRuntime = {
308
+ conversation: conversation as unknown as XmtpConversation,
309
+ message: message as unknown as XmtpMessage,
310
+ xmtpClient
311
+ }
312
+
313
+ const runtime = await agent.createRuntimeContext(baseRuntime)
314
+
315
+ // Execute pre-response behaviors
316
+ let behaviorContext: BehaviorContext | undefined
317
+ if (context.behaviors) {
318
+ behaviorContext = {
319
+ runtime,
320
+ client: xmtpClient as unknown as XmtpClient,
321
+ conversation: conversation as unknown as XmtpConversation,
322
+ message: message as unknown as XmtpMessage
323
+ }
324
+ await context.behaviors.executeBefore(behaviorContext)
325
+
326
+ // Check if behaviors were stopped early (e.g., due to filtering)
327
+ if (behaviorContext.stopped) {
328
+ logger.debug(
329
+ `🔇 [XMTP Plugin] Skipping reply response due to behavior chain being stopped`
330
+ )
331
+ return
332
+ }
333
+ }
334
+
335
+ const { text: reply } = await agent.generate(messages, { runtime })
336
+
337
+ // Execute post-response behaviors
338
+ if (context.behaviors) {
339
+ if (!behaviorContext) {
340
+ behaviorContext = {
341
+ runtime,
342
+ client: xmtpClient as unknown as XmtpClient,
343
+ conversation: conversation as unknown as XmtpConversation,
344
+ message: message as unknown as XmtpMessage,
345
+ response: reply
346
+ }
347
+ } else {
348
+ behaviorContext.response = reply
349
+ }
350
+ await context.behaviors.executeAfter(behaviorContext)
351
+
352
+ // Check if post behaviors were stopped early
353
+ if (behaviorContext.stopped) {
354
+ logger.debug(
355
+ `🔇 [XMTP Plugin] Skipping reply response due to post-behavior chain being stopped`
356
+ )
357
+ return
358
+ }
359
+ } else {
360
+ // Create minimal context for send options
361
+ behaviorContext = {
362
+ runtime,
363
+ client: xmtpClient as unknown as XmtpClient,
364
+ conversation: conversation as unknown as XmtpConversation,
365
+ message: message as unknown as XmtpMessage,
366
+ response: reply
367
+ }
368
+ }
369
+
370
+ await sendResponse(
371
+ conversation as unknown as XmtpConversation,
372
+ reply,
373
+ message.id,
374
+ behaviorContext
375
+ )
376
+ } catch (err) {
377
+ logger.error("❌ Error handling reply:", err)
378
+ }
379
+ })
380
+
381
+ xmtp.on("text", async ({ conversation, message }) => {
382
+ try {
383
+ const text = message.content
384
+ const messages: AgentMessage[] = [
385
+ { id: randomUUID(), role: "user", parts: [{ type: "text", text }] }
386
+ ]
387
+
388
+ const baseRuntime: AgentRuntime = {
389
+ conversation: conversation as unknown as XmtpConversation,
390
+ message: message as unknown as XmtpMessage,
391
+ xmtpClient
392
+ }
393
+
394
+ const runtime = await agent.createRuntimeContext(baseRuntime)
395
+
396
+ // Execute pre-response behaviors
397
+ let behaviorContext: BehaviorContext | undefined
398
+ if (context.behaviors) {
399
+ behaviorContext = {
400
+ runtime,
401
+ client: xmtpClient as unknown as XmtpClient,
402
+ conversation: conversation as unknown as XmtpConversation,
403
+ message: message as unknown as XmtpMessage
404
+ }
405
+ await context.behaviors.executeBefore(behaviorContext)
406
+
407
+ // Check if behaviors were stopped early (e.g., due to filtering)
408
+ if (behaviorContext.stopped) {
409
+ logger.debug(
410
+ `🔇 [XMTP Plugin] Skipping text response due to behavior chain being stopped`
411
+ )
412
+ return
413
+ }
414
+ }
415
+
416
+ const { text: reply } = await agent.generate(messages, { runtime })
417
+
418
+ // Execute post-response behaviors
419
+ if (context.behaviors) {
420
+ if (!behaviorContext) {
421
+ behaviorContext = {
422
+ runtime,
423
+ client: xmtpClient as unknown as XmtpClient,
424
+ conversation: conversation as unknown as XmtpConversation,
425
+ message: message as unknown as XmtpMessage,
426
+ response: reply
427
+ }
428
+ } else {
429
+ behaviorContext.response = reply
430
+ }
431
+ await context.behaviors.executeAfter(behaviorContext)
432
+
433
+ // Check if post behaviors were stopped early
434
+ if (behaviorContext.stopped) {
435
+ logger.debug(
436
+ `🔇 [XMTP Plugin] Skipping text response due to post-behavior chain being stopped`
437
+ )
438
+ return
439
+ }
440
+ } else {
441
+ // Create minimal context for send options
442
+ behaviorContext = {
443
+ runtime,
444
+ client: xmtpClient as unknown as XmtpClient,
445
+ conversation: conversation as unknown as XmtpConversation,
446
+ message: message as unknown as XmtpMessage,
447
+ response: reply
448
+ }
449
+ }
450
+
451
+ await sendResponse(
452
+ conversation as unknown as XmtpConversation,
453
+ reply,
454
+ message.id,
455
+ behaviorContext
456
+ )
457
+ } catch (err) {
458
+ logger.error("❌ Error handling text:", err)
459
+ }
460
+ })
461
+
462
+ // Event handlers removed due to incompatibility with current XMTP agent SDK
463
+
464
+ void xmtp
465
+ .start()
466
+ .then(() => logger.debug("✅ XMTP agent listener started"))
467
+ .catch((err) =>
468
+ console.error("❌ XMTP agent listener failed to start:", err)
469
+ )
37
470
  }
38
471
  }
39
472
  }