@xmtp/agent-sdk 1.2.4 → 1.2.5-rc1
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.
- package/dist/debug.d.ts +141 -1
- package/dist/debug.js +85 -2
- package/dist/debug.js.map +1 -1
- package/dist/index.d.ts +176 -4
- package/dist/index.js +703 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +81 -1
- package/dist/middleware.js +59 -2
- package/dist/middleware.js.map +1 -1
- package/dist/user.d.ts +16 -1
- package/dist/user.js +111 -2
- package/dist/user.js.map +1 -1
- package/dist/util.d.ts +46 -1
- package/dist/util.js +74 -2
- package/dist/util.js.map +1 -1
- package/package.json +14 -21
- package/dist/bin/generateKeys.d.ts +0 -2
- package/dist/bin/generateKeys.js +0 -44
- package/dist/bin/generateKeys.js.map +0 -1
- package/dist/core/Agent.d.ts +0 -84
- package/dist/core/Agent.js +0 -394
- package/dist/core/Agent.js.map +0 -1
- package/dist/core/AgentError.d.ts +0 -7
- package/dist/core/AgentError.js +0 -13
- package/dist/core/AgentError.js.map +0 -1
- package/dist/core/ClientContext.d.ts +0 -9
- package/dist/core/ClientContext.js +0 -13
- package/dist/core/ClientContext.js.map +0 -1
- package/dist/core/ConversationContext.d.ts +0 -19
- package/dist/core/ConversationContext.js +0 -43
- package/dist/core/ConversationContext.js.map +0 -1
- package/dist/core/MessageContext.d.ts +0 -33
- package/dist/core/MessageContext.js +0 -75
- package/dist/core/MessageContext.js.map +0 -1
- package/dist/core/filter.d.ts +0 -84
- package/dist/core/filter.js +0 -88
- package/dist/core/filter.js.map +0 -1
- package/dist/core/index.d.ts +0 -6
- package/dist/core/index.js +0 -7
- package/dist/core/index.js.map +0 -1
- package/dist/debug/index.d.ts +0 -1
- package/dist/debug/index.js +0 -2
- package/dist/debug/index.js.map +0 -1
- package/dist/debug/log.d.ts +0 -18
- package/dist/debug/log.js +0 -80
- package/dist/debug/log.js.map +0 -1
- package/dist/middleware/CommandRouter.d.ts +0 -14
- package/dist/middleware/CommandRouter.js +0 -57
- package/dist/middleware/CommandRouter.js.map +0 -1
- package/dist/middleware/index.d.ts +0 -1
- package/dist/middleware/index.js +0 -2
- package/dist/middleware/index.js.map +0 -1
- package/dist/user/NameResolver.d.ts +0 -1
- package/dist/user/NameResolver.js +0 -45
- package/dist/user/NameResolver.js.map +0 -1
- package/dist/user/User.d.ts +0 -10
- package/dist/user/User.js +0 -35
- package/dist/user/User.js.map +0 -1
- package/dist/user/index.d.ts +0 -2
- package/dist/user/index.js +0 -3
- package/dist/user/index.js.map +0 -1
- package/dist/util/AttachmentUtil.d.ts +0 -41
- package/dist/util/AttachmentUtil.js +0 -67
- package/dist/util/AttachmentUtil.js.map +0 -1
- package/dist/util/LimitedMap.d.ts +0 -6
- package/dist/util/LimitedMap.js +0 -20
- package/dist/util/LimitedMap.js.map +0 -1
- package/dist/util/TestUtil.d.ts +0 -35
- package/dist/util/TestUtil.js +0 -83
- package/dist/util/TestUtil.js.map +0 -1
- package/dist/util/index.d.ts +0 -1
- package/dist/util/index.js +0 -2
- package/dist/util/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,703 @@
|
|
|
1
|
-
|
|
2
|
-
export { isHexString, validHex } from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { Dm, Group, contentTypeGroupUpdated, contentTypeReaction, contentTypeReply, contentTypeRemoteAttachment, contentTypeMarkdown, contentTypeReadReceipt, contentTypeText, contentTypeTransactionReference, contentTypeWalletSendCalls, Client, encryptAttachment as encryptAttachment$1, encodeMarkdown, encodeText, isHexString, ApiUrls, DecodedMessage } from '@xmtp/node-sdk';
|
|
2
|
+
export { ConsentEntityType, ConsentState, ConversationType, DeliveryStatus, GroupMember, GroupMembershipState, GroupMessageKind, GroupMetadata, GroupPermissions, GroupPermissionsOptions, IdentifierKind, LogLevel, MetadataField, PermissionLevel, PermissionPolicy, PermissionUpdateType, SignatureRequestHandle, SortDirection, isHexString, validHex } from '@xmtp/node-sdk';
|
|
3
|
+
import EventEmitter from 'node:events';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { contentTypesAreEqual } from '@xmtp/content-type-primitives';
|
|
7
|
+
import { toBytes, createWalletClient, http } from 'viem';
|
|
8
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
9
|
+
import { sepolia } from 'viem/chains';
|
|
10
|
+
|
|
11
|
+
const fromSelf = (message, client) => {
|
|
12
|
+
return message.senderInboxId === client.inboxId;
|
|
13
|
+
};
|
|
14
|
+
const hasContent = (message) => {
|
|
15
|
+
return message.content !== undefined && message.content !== null;
|
|
16
|
+
};
|
|
17
|
+
const isDM = (conversation) => {
|
|
18
|
+
return conversation instanceof Dm;
|
|
19
|
+
};
|
|
20
|
+
const isGroup = (conversation) => {
|
|
21
|
+
return conversation instanceof Group;
|
|
22
|
+
};
|
|
23
|
+
const isGroupAdmin = (conversation, message) => {
|
|
24
|
+
if (isGroup(conversation)) {
|
|
25
|
+
return conversation.isAdmin(message.senderInboxId);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
const isGroupSuperAdmin = (conversation, message) => {
|
|
30
|
+
if (isGroup(conversation)) {
|
|
31
|
+
return conversation.isSuperAdmin(message.senderInboxId);
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
const isGroupUpdate = (message) => {
|
|
36
|
+
return contentTypesAreEqual(message.contentType, contentTypeGroupUpdated());
|
|
37
|
+
};
|
|
38
|
+
const isReaction = (message) => {
|
|
39
|
+
return contentTypesAreEqual(message.contentType, contentTypeReaction());
|
|
40
|
+
};
|
|
41
|
+
const isReply = (message) => {
|
|
42
|
+
return contentTypesAreEqual(message.contentType, contentTypeReply());
|
|
43
|
+
};
|
|
44
|
+
const isRemoteAttachment = (message) => {
|
|
45
|
+
return contentTypesAreEqual(message.contentType, contentTypeRemoteAttachment());
|
|
46
|
+
};
|
|
47
|
+
const isMarkdown = (message) => {
|
|
48
|
+
return contentTypesAreEqual(message.contentType, contentTypeMarkdown());
|
|
49
|
+
};
|
|
50
|
+
const isReadReceipt = (message) => {
|
|
51
|
+
return contentTypesAreEqual(message.contentType, contentTypeReadReceipt());
|
|
52
|
+
};
|
|
53
|
+
const isText = (message) => {
|
|
54
|
+
return contentTypesAreEqual(message.contentType, contentTypeText());
|
|
55
|
+
};
|
|
56
|
+
const isTextReply = (message) => {
|
|
57
|
+
return isReply(message) && typeof message.content.content === "string";
|
|
58
|
+
};
|
|
59
|
+
const isTransactionReference = (message) => {
|
|
60
|
+
return contentTypesAreEqual(message.contentType, contentTypeTransactionReference());
|
|
61
|
+
};
|
|
62
|
+
const isWalletSendCalls = (message) => {
|
|
63
|
+
return contentTypesAreEqual(message.contentType, contentTypeWalletSendCalls());
|
|
64
|
+
};
|
|
65
|
+
const usesCodec = (message, codecClass) => {
|
|
66
|
+
return contentTypesAreEqual(message.contentType, new codecClass().contentType);
|
|
67
|
+
};
|
|
68
|
+
const filter = {
|
|
69
|
+
fromSelf,
|
|
70
|
+
hasContent,
|
|
71
|
+
isDM,
|
|
72
|
+
isGroup,
|
|
73
|
+
isGroupAdmin,
|
|
74
|
+
isGroupSuperAdmin,
|
|
75
|
+
isGroupUpdate,
|
|
76
|
+
isMarkdown,
|
|
77
|
+
isReaction,
|
|
78
|
+
isReadReceipt,
|
|
79
|
+
isRemoteAttachment,
|
|
80
|
+
isReply,
|
|
81
|
+
isText,
|
|
82
|
+
isTextReply,
|
|
83
|
+
isTransactionReference,
|
|
84
|
+
isWalletSendCalls,
|
|
85
|
+
usesCodec,
|
|
86
|
+
};
|
|
87
|
+
const f = filter;
|
|
88
|
+
|
|
89
|
+
const getInstallationInfo = async (client) => {
|
|
90
|
+
const myInboxId = client.inboxId;
|
|
91
|
+
const myInstallationId = client.installationId;
|
|
92
|
+
const inboxStates = await Client.fetchInboxStates([myInboxId], client.options?.env);
|
|
93
|
+
const installations = inboxStates.find((state) => state.inboxId === myInboxId)?.installations ||
|
|
94
|
+
[];
|
|
95
|
+
const sortedInstallations = [...installations].sort((a, b) => {
|
|
96
|
+
const aTime = a.clientTimestampNs ?? 0n;
|
|
97
|
+
const bTime = b.clientTimestampNs ?? 0n;
|
|
98
|
+
return bTime > aTime ? 1 : bTime < aTime ? -1 : 0;
|
|
99
|
+
});
|
|
100
|
+
const mostRecentInstallation = sortedInstallations[0];
|
|
101
|
+
const myInstallationIdHex = Buffer.from(client.installationIdBytes).toString("hex");
|
|
102
|
+
const info = {
|
|
103
|
+
totalInstallations: installations.length,
|
|
104
|
+
installationId: myInstallationId,
|
|
105
|
+
mostRecentInstallationId: null,
|
|
106
|
+
isMostRecent: false,
|
|
107
|
+
};
|
|
108
|
+
if (mostRecentInstallation) {
|
|
109
|
+
const mostRecentIdHex = Buffer.from(mostRecentInstallation.bytes).toString("hex");
|
|
110
|
+
info.mostRecentInstallationId = mostRecentIdHex;
|
|
111
|
+
info.isMostRecent = myInstallationIdHex === mostRecentIdHex;
|
|
112
|
+
}
|
|
113
|
+
return info;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const createUser = (key, chain = sepolia) => {
|
|
117
|
+
const accountKey = key ?? generatePrivateKey();
|
|
118
|
+
const account = privateKeyToAccount(accountKey);
|
|
119
|
+
return {
|
|
120
|
+
key: accountKey,
|
|
121
|
+
account,
|
|
122
|
+
wallet: createWalletClient({
|
|
123
|
+
account,
|
|
124
|
+
chain,
|
|
125
|
+
transport: http(),
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
const createIdentifier = (user) => ({
|
|
130
|
+
identifier: user.account.address.toLowerCase(),
|
|
131
|
+
identifierKind: 0 /* IdentifierKind.Ethereum */,
|
|
132
|
+
});
|
|
133
|
+
const createSigner = (user) => {
|
|
134
|
+
const identifier = createIdentifier(user);
|
|
135
|
+
return {
|
|
136
|
+
type: "EOA",
|
|
137
|
+
getIdentifier: () => identifier,
|
|
138
|
+
signMessage: async (message) => {
|
|
139
|
+
const signature = await user.wallet.signMessage({
|
|
140
|
+
account: user.account,
|
|
141
|
+
message,
|
|
142
|
+
});
|
|
143
|
+
return toBytes(signature);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
class AgentError extends Error {
|
|
149
|
+
#code;
|
|
150
|
+
constructor(code, message, cause) {
|
|
151
|
+
super(message, { cause });
|
|
152
|
+
this.#code = code;
|
|
153
|
+
}
|
|
154
|
+
get code() {
|
|
155
|
+
return this.#code;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
class AgentStreamingError extends AgentError {
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class ClientContext {
|
|
162
|
+
#client;
|
|
163
|
+
constructor({ client }) {
|
|
164
|
+
this.#client = client;
|
|
165
|
+
}
|
|
166
|
+
getClientAddress() {
|
|
167
|
+
return this.#client.accountIdentifier?.identifier;
|
|
168
|
+
}
|
|
169
|
+
get client() {
|
|
170
|
+
return this.#client;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Encrypts an attachment for secure remote storage.
|
|
176
|
+
*
|
|
177
|
+
* @param attachmentData - The attachment to encrypt, including its data, filename, and MIME type
|
|
178
|
+
* @returns A promise that resolves with the encrypted attachment containing the encrypted content and metadata
|
|
179
|
+
*/
|
|
180
|
+
function encryptAttachment(attachmentData) {
|
|
181
|
+
const encryptedAttachment = encryptAttachment$1(attachmentData);
|
|
182
|
+
return {
|
|
183
|
+
content: encryptedAttachment,
|
|
184
|
+
mimeType: attachmentData.mimeType,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Creates a remote attachment object from an encrypted attachment and file URL.
|
|
189
|
+
*
|
|
190
|
+
* @param encryptedAttachment - The encrypted attachment containing encryption keys and metadata
|
|
191
|
+
* @param fileUrl - The URL where the encrypted attachment can be downloaded
|
|
192
|
+
* @returns A remote attachment object with all necessary metadata for retrieval and decryption
|
|
193
|
+
*/
|
|
194
|
+
function createRemoteAttachment(encryptedAttachment, fileUrl) {
|
|
195
|
+
const url = new URL(fileUrl);
|
|
196
|
+
return {
|
|
197
|
+
url: url.toString(),
|
|
198
|
+
contentDigest: encryptedAttachment.content.contentDigest,
|
|
199
|
+
salt: encryptedAttachment.content.salt,
|
|
200
|
+
nonce: encryptedAttachment.content.nonce,
|
|
201
|
+
secret: encryptedAttachment.content.secret,
|
|
202
|
+
scheme: url.protocol,
|
|
203
|
+
contentLength: encryptedAttachment.content.payload.length,
|
|
204
|
+
filename: encryptedAttachment.content.filename,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Creates a remote attachment from a file by encrypting it and uploading it to a remote storage.
|
|
209
|
+
* This is a convenience function that combines file processing, encryption, uploading, and
|
|
210
|
+
* remote attachment creation into a single operation.
|
|
211
|
+
*
|
|
212
|
+
* @param unencryptedFile - The unencrypted file to process and upload
|
|
213
|
+
* @param uploadCallback - A callback function that receives the encrypted attachment and returns the URL where it was uploaded
|
|
214
|
+
* @returns A promise that resolves with a remote attachment containing all necessary metadata for retrieval and decryption
|
|
215
|
+
*/
|
|
216
|
+
async function createRemoteAttachmentFromFile(unencryptedFile, uploadCallback) {
|
|
217
|
+
const arrayBuffer = await unencryptedFile.arrayBuffer();
|
|
218
|
+
const attachment = new Uint8Array(arrayBuffer);
|
|
219
|
+
const attachmentData = {
|
|
220
|
+
content: attachment,
|
|
221
|
+
filename: unencryptedFile.name,
|
|
222
|
+
mimeType: unencryptedFile.type,
|
|
223
|
+
};
|
|
224
|
+
const encryptedAttachment = encryptAttachment(attachmentData);
|
|
225
|
+
const fileUrl = await uploadCallback(encryptedAttachment);
|
|
226
|
+
return createRemoteAttachment(encryptedAttachment, fileUrl);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class ConversationContext extends ClientContext {
|
|
230
|
+
#conversation;
|
|
231
|
+
constructor({ conversation, client, }) {
|
|
232
|
+
super({ client });
|
|
233
|
+
this.#conversation = conversation;
|
|
234
|
+
}
|
|
235
|
+
isDm() {
|
|
236
|
+
return filter.isDM(this.#conversation);
|
|
237
|
+
}
|
|
238
|
+
isGroup() {
|
|
239
|
+
return filter.isGroup(this.#conversation);
|
|
240
|
+
}
|
|
241
|
+
// Send methods, which don't need a message context, are in ConversationContext to make them available in both dm and group event handlers
|
|
242
|
+
async sendMarkdown(markdown) {
|
|
243
|
+
await this.#conversation.sendMarkdown(markdown);
|
|
244
|
+
}
|
|
245
|
+
async sendText(text) {
|
|
246
|
+
await this.#conversation.sendText(text);
|
|
247
|
+
}
|
|
248
|
+
async sendRemoteAttachment(unencryptedFile, uploadCallback) {
|
|
249
|
+
const remoteAttachment = await createRemoteAttachmentFromFile(unencryptedFile, uploadCallback);
|
|
250
|
+
await this.#conversation.sendRemoteAttachment(remoteAttachment);
|
|
251
|
+
}
|
|
252
|
+
get conversation() {
|
|
253
|
+
return this.#conversation;
|
|
254
|
+
}
|
|
255
|
+
get isAllowed() {
|
|
256
|
+
return this.#conversation.consentState() === 1 /* ConsentState.Allowed */;
|
|
257
|
+
}
|
|
258
|
+
get isDenied() {
|
|
259
|
+
return this.#conversation.consentState() === 2 /* ConsentState.Denied */;
|
|
260
|
+
}
|
|
261
|
+
get isUnknown() {
|
|
262
|
+
return this.#conversation.consentState() === 0 /* ConsentState.Unknown */;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class MessageContext extends ConversationContext {
|
|
267
|
+
#message;
|
|
268
|
+
constructor({ message, conversation, client, }) {
|
|
269
|
+
super({ conversation, client });
|
|
270
|
+
this.#message = message;
|
|
271
|
+
}
|
|
272
|
+
usesCodec(codecClass) {
|
|
273
|
+
return filter.usesCodec(this.#message, codecClass);
|
|
274
|
+
}
|
|
275
|
+
isMarkdown() {
|
|
276
|
+
return filter.isMarkdown(this.#message);
|
|
277
|
+
}
|
|
278
|
+
isText() {
|
|
279
|
+
return filter.isText(this.#message);
|
|
280
|
+
}
|
|
281
|
+
isReply() {
|
|
282
|
+
return filter.isReply(this.#message);
|
|
283
|
+
}
|
|
284
|
+
isReaction() {
|
|
285
|
+
return filter.isReaction(this.#message);
|
|
286
|
+
}
|
|
287
|
+
isReadReceipt() {
|
|
288
|
+
return filter.isReadReceipt(this.#message);
|
|
289
|
+
}
|
|
290
|
+
isRemoteAttachment() {
|
|
291
|
+
return filter.isRemoteAttachment(this.#message);
|
|
292
|
+
}
|
|
293
|
+
isTransactionReference() {
|
|
294
|
+
return filter.isTransactionReference(this.#message);
|
|
295
|
+
}
|
|
296
|
+
isWalletSendCalls() {
|
|
297
|
+
return filter.isWalletSendCalls(this.#message);
|
|
298
|
+
}
|
|
299
|
+
async sendReaction(content, schema = 1 /* ReactionSchema.Unicode */) {
|
|
300
|
+
const reaction = {
|
|
301
|
+
action: 1 /* ReactionAction.Added */,
|
|
302
|
+
reference: this.#message.id,
|
|
303
|
+
referenceInboxId: this.#message.senderInboxId,
|
|
304
|
+
schema,
|
|
305
|
+
content,
|
|
306
|
+
};
|
|
307
|
+
await this.conversation.sendReaction(reaction);
|
|
308
|
+
}
|
|
309
|
+
async #sendReply(content) {
|
|
310
|
+
await this.conversation.sendReply({
|
|
311
|
+
content,
|
|
312
|
+
reference: this.#message.id,
|
|
313
|
+
referenceInboxId: this.#message.senderInboxId,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async sendMarkdownReply(markdown) {
|
|
317
|
+
await this.#sendReply(encodeMarkdown(markdown));
|
|
318
|
+
}
|
|
319
|
+
async sendTextReply(text) {
|
|
320
|
+
await this.#sendReply(encodeText(text));
|
|
321
|
+
}
|
|
322
|
+
async getSenderAddress() {
|
|
323
|
+
const inboxState = await this.client.preferences.getInboxStates([
|
|
324
|
+
this.#message.senderInboxId,
|
|
325
|
+
]);
|
|
326
|
+
return inboxState[0]?.identifiers[0]?.identifier;
|
|
327
|
+
}
|
|
328
|
+
get message() {
|
|
329
|
+
return this.#message;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
class Agent extends EventEmitter {
|
|
334
|
+
#client;
|
|
335
|
+
#conversationsStream;
|
|
336
|
+
#messageStream;
|
|
337
|
+
#middleware = [];
|
|
338
|
+
#errorMiddleware = [];
|
|
339
|
+
#errors = Object.freeze({
|
|
340
|
+
use: (...errorMiddleware) => {
|
|
341
|
+
for (const emw of errorMiddleware) {
|
|
342
|
+
if (Array.isArray(emw)) {
|
|
343
|
+
this.#errorMiddleware.push(...emw);
|
|
344
|
+
}
|
|
345
|
+
else if (typeof emw === "function") {
|
|
346
|
+
this.#errorMiddleware.push(emw);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return this.#errors;
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
#defaultErrorHandler = (currentError) => {
|
|
353
|
+
const emittedError = currentError instanceof Error
|
|
354
|
+
? currentError
|
|
355
|
+
: new AgentError(9999, `Unhandled error caught by default error middleware.`, currentError);
|
|
356
|
+
this.emit("unhandledError", emittedError);
|
|
357
|
+
};
|
|
358
|
+
#isLocked = false;
|
|
359
|
+
constructor({ client }) {
|
|
360
|
+
super();
|
|
361
|
+
this.#client = client;
|
|
362
|
+
}
|
|
363
|
+
static async create(signer,
|
|
364
|
+
// Note: we need to omit this so that "Client.create" can correctly infer the codecs.
|
|
365
|
+
options) {
|
|
366
|
+
const initializedOptions = { ...(options ?? {}) };
|
|
367
|
+
initializedOptions.appVersion ??= "agent-sdk/alpha";
|
|
368
|
+
initializedOptions.disableDeviceSync ??= true;
|
|
369
|
+
if (process.env.XMTP_FORCE_DEBUG) {
|
|
370
|
+
const loggingLevel = process.env.XMTP_FORCE_DEBUG_LEVEL || "Warn" /* LogLevel.Warn */;
|
|
371
|
+
initializedOptions.loggingLevel = loggingLevel;
|
|
372
|
+
initializedOptions.structuredLogging = true;
|
|
373
|
+
}
|
|
374
|
+
const client = await Client.create(signer, {
|
|
375
|
+
...initializedOptions,
|
|
376
|
+
codecs: initializedOptions.codecs,
|
|
377
|
+
});
|
|
378
|
+
const info = await getInstallationInfo(client);
|
|
379
|
+
if (info.totalInstallations > 1 && info.isMostRecent) {
|
|
380
|
+
console.warn(`[WARNING] You have "${info.totalInstallations}" installations. Installation ID "${info.installationId}" is the most recent. Make sure to persist and reload your installation data. If you exceed the installation limit, your Agent will stop working. Read more: https://docs.xmtp.org/agents/build-agents/local-database#installation-limits-and-revocation-rules`);
|
|
381
|
+
}
|
|
382
|
+
return new Agent({ client });
|
|
383
|
+
}
|
|
384
|
+
static async createFromEnv(
|
|
385
|
+
// Note: we need to omit this so that "Client.create" can correctly infer the codecs.
|
|
386
|
+
options) {
|
|
387
|
+
const { XMTP_DB_DIRECTORY, XMTP_DB_ENCRYPTION_KEY, XMTP_ENV, XMTP_WALLET_KEY, } = process.env;
|
|
388
|
+
if (!isHexString(XMTP_WALLET_KEY)) {
|
|
389
|
+
throw new AgentError(1000, `XMTP_WALLET_KEY env is not in hex (0x) format.`);
|
|
390
|
+
}
|
|
391
|
+
const signer = createSigner(createUser(XMTP_WALLET_KEY));
|
|
392
|
+
const initializedOptions = { ...(options ?? {}) };
|
|
393
|
+
initializedOptions.dbEncryptionKey =
|
|
394
|
+
typeof XMTP_DB_ENCRYPTION_KEY === "string"
|
|
395
|
+
? isHexString(XMTP_DB_ENCRYPTION_KEY)
|
|
396
|
+
? XMTP_DB_ENCRYPTION_KEY
|
|
397
|
+
: `0x${XMTP_DB_ENCRYPTION_KEY}`
|
|
398
|
+
: undefined;
|
|
399
|
+
if (XMTP_ENV && Object.keys(ApiUrls).includes(XMTP_ENV)) {
|
|
400
|
+
initializedOptions.env = XMTP_ENV;
|
|
401
|
+
}
|
|
402
|
+
if (typeof XMTP_DB_DIRECTORY === "string") {
|
|
403
|
+
fs.mkdirSync(XMTP_DB_DIRECTORY, { recursive: true, mode: 0o700 });
|
|
404
|
+
initializedOptions.dbPath = (inboxId) => {
|
|
405
|
+
const dbPath = path.join(XMTP_DB_DIRECTORY, `xmtp-${inboxId}.db3`);
|
|
406
|
+
console.info(`Saving local database to "${dbPath}"`);
|
|
407
|
+
return dbPath;
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return this.create(signer, initializedOptions);
|
|
411
|
+
}
|
|
412
|
+
get libxmtpVersion() {
|
|
413
|
+
return this.#client.libxmtpVersion;
|
|
414
|
+
}
|
|
415
|
+
use(...middleware) {
|
|
416
|
+
for (const mw of middleware) {
|
|
417
|
+
if (Array.isArray(mw)) {
|
|
418
|
+
this.#middleware.push(...mw);
|
|
419
|
+
}
|
|
420
|
+
else if (typeof mw === "function") {
|
|
421
|
+
this.#middleware.push(mw);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
async #stopStreams() {
|
|
427
|
+
try {
|
|
428
|
+
await this.#conversationsStream?.end();
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
this.#conversationsStream = undefined;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
await this.#messageStream?.end();
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
this.#messageStream = undefined;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Closes all existing streams and restarts the streaming system.
|
|
442
|
+
*/
|
|
443
|
+
async #handleStreamError(error) {
|
|
444
|
+
await this.#stopStreams();
|
|
445
|
+
const recovered = await this.#runErrorChain(error, {
|
|
446
|
+
client: this.#client,
|
|
447
|
+
});
|
|
448
|
+
if (recovered) {
|
|
449
|
+
this.#isLocked = false;
|
|
450
|
+
queueMicrotask(() => this.start());
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async start(options) {
|
|
454
|
+
if (this.#isLocked || this.#conversationsStream || this.#messageStream)
|
|
455
|
+
return;
|
|
456
|
+
this.#isLocked = true;
|
|
457
|
+
try {
|
|
458
|
+
this.#conversationsStream = await this.#client.conversations.stream({
|
|
459
|
+
...options,
|
|
460
|
+
onValue: async (conversation) => {
|
|
461
|
+
try {
|
|
462
|
+
if (!conversation) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.emit("conversation", new ConversationContext({
|
|
466
|
+
conversation,
|
|
467
|
+
client: this.#client,
|
|
468
|
+
}));
|
|
469
|
+
if (conversation instanceof Group) {
|
|
470
|
+
this.emit("group", new ConversationContext({
|
|
471
|
+
conversation,
|
|
472
|
+
client: this.#client,
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
else if (conversation instanceof Dm) {
|
|
476
|
+
this.emit("dm", new ConversationContext({
|
|
477
|
+
conversation,
|
|
478
|
+
client: this.#client,
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
const recovered = await this.#runErrorChain(new AgentError(1001, "Emitted value from conversation stream caused an error.", error), new ClientContext({ client: this.#client }));
|
|
484
|
+
if (!recovered)
|
|
485
|
+
await this.stop();
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
onError: async (error) => {
|
|
489
|
+
const recovered = await this.#runErrorChain(new AgentStreamingError(1002, "Error occured during conversation streaming.", error), new ClientContext({ client: this.#client }));
|
|
490
|
+
if (!recovered)
|
|
491
|
+
await this.stop();
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
this.#messageStream = await this.#client.conversations.streamAllMessages({
|
|
495
|
+
...options,
|
|
496
|
+
onValue: async (message) => {
|
|
497
|
+
// this case should not happen,
|
|
498
|
+
// but we must handle it for proper types
|
|
499
|
+
if (!(message instanceof DecodedMessage)) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
switch (true) {
|
|
504
|
+
case filter.isGroupUpdate(message):
|
|
505
|
+
await this.#processMessage(message, "group-update");
|
|
506
|
+
break;
|
|
507
|
+
case filter.isRemoteAttachment(message):
|
|
508
|
+
await this.#processMessage(message, "attachment");
|
|
509
|
+
break;
|
|
510
|
+
case filter.isReaction(message):
|
|
511
|
+
await this.#processMessage(message, "reaction");
|
|
512
|
+
break;
|
|
513
|
+
case filter.isReadReceipt(message):
|
|
514
|
+
await this.#processMessage(message, "read-receipt");
|
|
515
|
+
break;
|
|
516
|
+
case filter.isReply(message):
|
|
517
|
+
await this.#processMessage(message, "reply");
|
|
518
|
+
break;
|
|
519
|
+
case filter.isTransactionReference(message):
|
|
520
|
+
await this.#processMessage(message, "transaction-reference");
|
|
521
|
+
break;
|
|
522
|
+
case filter.isWalletSendCalls(message):
|
|
523
|
+
await this.#processMessage(message, "wallet-send-calls");
|
|
524
|
+
break;
|
|
525
|
+
case filter.isMarkdown(message):
|
|
526
|
+
await this.#processMessage(message, "markdown");
|
|
527
|
+
break;
|
|
528
|
+
case filter.isText(message):
|
|
529
|
+
await this.#processMessage(message, "text");
|
|
530
|
+
break;
|
|
531
|
+
default:
|
|
532
|
+
await this.#processMessage(message);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
const recovered = await this.#runErrorChain(error, {
|
|
538
|
+
client: this.#client,
|
|
539
|
+
});
|
|
540
|
+
if (!recovered) {
|
|
541
|
+
await this.stop();
|
|
542
|
+
}
|
|
543
|
+
this.#isLocked = false;
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
onError: async (error) => {
|
|
547
|
+
const recovered = await this.#runErrorChain(new AgentStreamingError(1004, "Error occured during message streaming.", error), new ClientContext({ client: this.#client }));
|
|
548
|
+
if (!recovered)
|
|
549
|
+
await this.stop();
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
this.emit("start", new ClientContext({ client: this.#client }));
|
|
553
|
+
this.#isLocked = false;
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
await this.#handleStreamError(error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async #processMessage(message, topic = "unknownMessage") {
|
|
560
|
+
// Skip messages with undefined content (failed to decode)
|
|
561
|
+
if (!filter.hasContent(message)) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// Skip messages from agent itself
|
|
565
|
+
if (filter.fromSelf(message, this.#client)) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const conversation = await this.#client.conversations.getConversationById(message.conversationId);
|
|
569
|
+
if (!conversation) {
|
|
570
|
+
throw new AgentError(1003, `Failed to process message ID "${message.id}" for conversation ID "${message.conversationId}" because the conversation could not be found.`);
|
|
571
|
+
}
|
|
572
|
+
const context = new MessageContext({
|
|
573
|
+
message,
|
|
574
|
+
conversation,
|
|
575
|
+
client: this.#client,
|
|
576
|
+
});
|
|
577
|
+
await this.#runMiddlewareChain(context, topic);
|
|
578
|
+
}
|
|
579
|
+
async #runMiddlewareChain(context, topic = "unknownMessage") {
|
|
580
|
+
const finalEmit = async () => {
|
|
581
|
+
try {
|
|
582
|
+
this.emit(topic, context);
|
|
583
|
+
this.emit("message", context);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
await this.#runErrorChain(error, context);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const chain = this.#middleware.reduceRight((next, mw) => {
|
|
590
|
+
return async () => {
|
|
591
|
+
try {
|
|
592
|
+
await mw(context, next);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
const resume = await this.#runErrorChain(error, context);
|
|
596
|
+
if (resume) {
|
|
597
|
+
await next();
|
|
598
|
+
}
|
|
599
|
+
// Chain is not resuming, error is being swallowed
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}, finalEmit);
|
|
603
|
+
await chain();
|
|
604
|
+
}
|
|
605
|
+
async #runErrorHandler(handler, context, error) {
|
|
606
|
+
let settled = false;
|
|
607
|
+
let flow = { kind: "stopped" };
|
|
608
|
+
const next = (nextErr) => {
|
|
609
|
+
if (settled)
|
|
610
|
+
return;
|
|
611
|
+
settled = true;
|
|
612
|
+
flow =
|
|
613
|
+
nextErr === undefined
|
|
614
|
+
? { kind: "handled" }
|
|
615
|
+
: { kind: "continue", error: nextErr };
|
|
616
|
+
};
|
|
617
|
+
try {
|
|
618
|
+
await handler(error, context, next);
|
|
619
|
+
return flow;
|
|
620
|
+
}
|
|
621
|
+
catch (thrown) {
|
|
622
|
+
if (settled) {
|
|
623
|
+
return flow;
|
|
624
|
+
}
|
|
625
|
+
return { kind: "continue", error: thrown };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async #runErrorChain(error, context) {
|
|
629
|
+
const chain = [...this.#errorMiddleware, this.#defaultErrorHandler];
|
|
630
|
+
let currentError = error;
|
|
631
|
+
for (let i = 0; i < chain.length; i++) {
|
|
632
|
+
const handler = chain[i];
|
|
633
|
+
if (!handler)
|
|
634
|
+
continue;
|
|
635
|
+
const outcome = await this.#runErrorHandler(handler, context, currentError);
|
|
636
|
+
switch (outcome.kind) {
|
|
637
|
+
case "handled":
|
|
638
|
+
// Error was handled. Main middleware can continue.
|
|
639
|
+
return true;
|
|
640
|
+
case "stopped":
|
|
641
|
+
// Error cannot be handled. Main middleware won't continue.
|
|
642
|
+
return false;
|
|
643
|
+
case "continue":
|
|
644
|
+
// Error is passed to the next handler
|
|
645
|
+
currentError = outcome.error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Reached end of chain without recovery
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
get client() {
|
|
652
|
+
return this.#client;
|
|
653
|
+
}
|
|
654
|
+
get errors() {
|
|
655
|
+
return this.#errors;
|
|
656
|
+
}
|
|
657
|
+
async stop() {
|
|
658
|
+
this.#isLocked = true;
|
|
659
|
+
await this.#stopStreams();
|
|
660
|
+
this.emit("stop", new ClientContext({ client: this.#client }));
|
|
661
|
+
this.#isLocked = false;
|
|
662
|
+
}
|
|
663
|
+
createDmWithAddress(address, options) {
|
|
664
|
+
return this.#client.conversations.createDmWithIdentifier({
|
|
665
|
+
identifier: address,
|
|
666
|
+
identifierKind: 0 /* IdentifierKind.Ethereum */,
|
|
667
|
+
}, options);
|
|
668
|
+
}
|
|
669
|
+
createGroupWithAddresses(addresses, options) {
|
|
670
|
+
const identifiers = addresses.map((address) => {
|
|
671
|
+
return {
|
|
672
|
+
identifier: address,
|
|
673
|
+
identifierKind: 0 /* IdentifierKind.Ethereum */,
|
|
674
|
+
};
|
|
675
|
+
});
|
|
676
|
+
return this.#client.conversations.createGroupWithIdentifiers(identifiers, options);
|
|
677
|
+
}
|
|
678
|
+
addMembersWithAddresses(group, addresses) {
|
|
679
|
+
const identifiers = addresses.map((address) => {
|
|
680
|
+
return {
|
|
681
|
+
identifier: address,
|
|
682
|
+
identifierKind: 0 /* IdentifierKind.Ethereum */,
|
|
683
|
+
};
|
|
684
|
+
});
|
|
685
|
+
return group.addMembersByIdentifiers(identifiers);
|
|
686
|
+
}
|
|
687
|
+
async getConversationContext(conversationId) {
|
|
688
|
+
const conversation = await this.client.conversations.getConversationById(conversationId);
|
|
689
|
+
if (conversation) {
|
|
690
|
+
const context = new ConversationContext({
|
|
691
|
+
conversation,
|
|
692
|
+
client: this.#client,
|
|
693
|
+
});
|
|
694
|
+
return context;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
get address() {
|
|
698
|
+
return this.#client.accountIdentifier?.identifier;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export { Agent, AgentError, AgentStreamingError, ClientContext, ConversationContext, MessageContext, f, filter };
|
|
703
|
+
//# sourceMappingURL=index.js.map
|