create-message-kit 1.0.16 → 1.0.17

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-message-kit",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,52 +1,39 @@
1
1
  import { HandlerContext } from "@xmtp/message-kit";
2
- import { Client } from "@xmtp/xmtp-js";
3
- import { getUserInfo, getInfoCache, isOnXMTP } from "../lib/resolver.js";
4
- import { textGeneration, responseParser } from "../lib/openai.js";
2
+ import { getUserInfo, clearInfoCache, isOnXMTP } from "../lib/resolver.js";
3
+ import { textGeneration } from "../lib/openai.js";
4
+ import { processResponseWithSkill } from "../lib/openai.js";
5
+ import { isAddress } from "viem";
5
6
  import { ens_agent_prompt } from "../prompt.js";
6
- import type {
7
- ensDomain,
8
- converseUsername,
9
- tipAddress,
10
- chatHistories,
11
- tipDomain,
12
- } from "../lib/types.js";
13
- import { frameUrl, ensUrl, baseTxUrl, InfoCache } from "../lib/types.js";
14
-
15
- let tipAddress: tipAddress = {};
16
- let tipDomain: tipDomain = {};
17
- let ensDomain: ensDomain = {};
18
- let infoCache: InfoCache = {};
19
- let converseUsername: converseUsername = {};
20
- let chatHistories: chatHistories = {};
21
-
22
- // URL for the send transaction
7
+ import { frameUrl, ensUrl, baseTxUrl } from "../index.js";
8
+ import { clearChatHistories } from "../lib/openai.js";
9
+
23
10
  export async function handleEns(context: HandlerContext) {
24
11
  const {
25
12
  message: {
26
13
  content: { command, params, sender },
27
14
  },
28
15
  } = context;
29
-
30
- if (command == "renew") {
16
+ if (command == "reset") {
17
+ clear();
18
+ return { code: 200, message: "Conversation reset." };
19
+ } else if (command == "renew") {
31
20
  // Destructure and validate parameters for the ens command
32
21
  const { domain } = params;
33
22
  // Check if the user holds the domain
34
23
  if (!domain) {
35
- context.reply("Missing required parameters. Please provide domain.");
36
- return;
24
+ return {
25
+ code: 400,
26
+ message: "Missing required parameters. Please provide domain.",
27
+ };
37
28
  }
38
29
 
39
- const { infoCache: retrievedInfoCache } = await getInfoCache(
40
- domain,
41
- infoCache,
42
- );
43
- infoCache = retrievedInfoCache;
44
- let data = infoCache[domain].info;
30
+ const data = await getUserInfo(domain);
45
31
 
46
- if (data?.address !== sender?.address) {
32
+ if (!data || data?.address !== sender?.address) {
47
33
  return {
48
34
  code: 403,
49
- message: "You do not hold this domain. Only the owner can renew it.",
35
+ message:
36
+ "Looks like this domain is not registered to you. Only the owner can renew it.",
50
37
  };
51
38
  }
52
39
 
@@ -69,22 +56,23 @@ export async function handleEns(context: HandlerContext) {
69
56
  } else if (command == "info") {
70
57
  const { domain } = params;
71
58
 
72
- const { infoCache: retrievedInfoCache } = await getInfoCache(
73
- domain,
74
- infoCache,
75
- );
76
- infoCache = retrievedInfoCache;
77
- let data = infoCache[domain].info;
59
+ const data = await getUserInfo(domain);
60
+ if (!data) {
61
+ return {
62
+ code: 404,
63
+ message: "Domain not found.",
64
+ };
65
+ }
78
66
 
79
67
  const formattedData = {
80
68
  Address: data?.address,
81
- "Avatar URL": data?.avatar_url,
82
- Description: data?.description,
83
- ENS: data?.ens,
84
- "Primary ENS": data?.ens_primary,
85
- GitHub: data?.github,
86
- Resolver: data?.resolverAddress,
87
- Twitter: data?.twitter,
69
+ "Avatar URL": data?.ensInfo?.avatar,
70
+ Description: data?.ensInfo?.description,
71
+ ENS: data?.ensDomain,
72
+ "Primary ENS": data?.ensInfo?.ens_primary,
73
+ GitHub: data?.ensInfo?.github,
74
+ Resolver: data?.ensInfo?.resolverAddress,
75
+ Twitter: data?.ensInfo?.twitter,
88
76
  URL: `${ensUrl}${domain}`,
89
77
  };
90
78
 
@@ -96,23 +84,20 @@ export async function handleEns(context: HandlerContext) {
96
84
  }
97
85
  message += `\n\nWould you like to tip the domain owner for getting there first 🤣?`;
98
86
  message = message.trim();
99
- if (await isOnXMTP(context.v2client, data?.ens, data?.address)) {
100
- context.send(
87
+ if (
88
+ await isOnXMTP(
89
+ context.v2client,
90
+ data?.ensInfo?.ens,
91
+ data?.ensInfo?.address,
92
+ )
93
+ ) {
94
+ await context.send(
101
95
  `Ah, this domains is in XMTP, you can message it directly: https://converse.xyz/dm/${domain}`,
102
96
  );
103
97
  }
104
98
  return { code: 200, message };
105
99
  } else if (command == "check") {
106
- console.log(params);
107
- const { domain, cool_alternatives } = params;
108
-
109
- const cool_alternativesFormat = cool_alternatives
110
- ?.split(",")
111
- .map(
112
- (alternative: string, index: number) =>
113
- `${index + 1}. ${alternative} ✨`,
114
- )
115
- .join("\n");
100
+ const { domain } = params;
116
101
 
117
102
  if (!domain) {
118
103
  return {
@@ -120,82 +105,46 @@ export async function handleEns(context: HandlerContext) {
120
105
  message: "Please provide a domain name to check.",
121
106
  };
122
107
  }
123
- const { infoCache: retrievedInfoCache } = await getInfoCache(
124
- domain,
125
- infoCache,
126
- );
127
- infoCache = retrievedInfoCache;
128
- let data = infoCache?.[domain]?.info;
108
+
109
+ const data = await getUserInfo(domain);
129
110
  if (!data?.address) {
130
- let message = `Looks like ${domain} is available! Do you want to register it? ${ensUrl}${domain}`;
111
+ let message = `Looks like ${domain} is available! Do you want to register it? ${ensUrl}${domain} or would you like to see some cool alternatives?`;
131
112
  return {
132
113
  code: 200,
133
114
  message,
134
115
  };
135
116
  } else {
136
- let message = `Looks like ${domain} is already registered! What about these cool alternatives?\n\n${cool_alternativesFormat}`;
117
+ let message = `Looks like ${domain} is already registered!`;
118
+ await context.skill("/cool " + domain);
137
119
  return {
138
120
  code: 404,
139
121
  message,
140
122
  };
141
123
  }
142
124
  } else if (command == "tip") {
143
- // Destructure and validate parameters for the send command
144
125
  const { address } = params;
145
-
146
- const { infoCache: retrievedInfoCache } = await getInfoCache(
147
- address,
148
- infoCache,
149
- );
150
- infoCache = retrievedInfoCache;
151
- let data = infoCache[address].info;
152
-
153
- tipAddress[sender.address] = data?.address;
154
- tipDomain[sender.address] = data?.ens;
155
-
156
- if (!address || !tipAddress[sender.address]) {
157
- context.reply("Missing required parameters. Please provide address.");
158
- return;
126
+ if (!address) {
127
+ return {
128
+ code: 400,
129
+ message: "Please provide an address to tip.",
130
+ };
159
131
  }
160
- let txUrl = `${baseTxUrl}/transaction/?transaction_type=send&buttonName=Tip%20${tipDomain[sender.address]}&amount=1&token=USDC&receiver=${tipAddress[sender.address]}`;
161
- // Generate URL for the send transaction
162
- context.send(`Here is the url to send the tip:\n${txUrl}`);
132
+ const data = await getUserInfo(address);
133
+ let txUrl = `${baseTxUrl}/transaction/?transaction_type=send&buttonName=Tip%20${data?.ensDomain ?? ""}&amount=1&token=USDC&receiver=${
134
+ isAddress(address) ? address : data?.address
135
+ }`;
136
+ console.log(txUrl);
137
+ return {
138
+ code: 200,
139
+ message: txUrl,
140
+ };
163
141
  } else if (command == "cool") {
164
- return;
165
- }
166
- }
167
-
168
- export async function clearChatHistory() {
169
- chatHistories = {};
170
- }
171
-
172
- async function processResponseWithIntent(
173
- reply: string,
174
- context: any,
175
- senderAddress: string,
176
- ) {
177
- let messages = reply
178
- .split("\n")
179
- .map((message: string) => responseParser(message))
180
- .filter((message): message is string => message.length > 0);
181
-
182
- console.log(messages);
183
- for (const message of messages) {
184
- if (message.startsWith("/")) {
185
- const response = await context.intent(message);
186
- if (response && response.message) {
187
- let msg = responseParser(response.message);
188
-
189
- chatHistories[senderAddress].push({
190
- role: "system",
191
- content: msg,
192
- });
193
-
194
- await context.send(response.message);
195
- }
196
- } else {
197
- await context.send(message);
198
- }
142
+ const { domain } = params;
143
+ //What about these cool alternatives?\
144
+ return {
145
+ code: 200,
146
+ message: `${generateCoolAlternatives(domain)}`,
147
+ };
199
148
  }
200
149
  }
201
150
 
@@ -215,33 +164,48 @@ export async function ensAgent(context: HandlerContext) {
215
164
 
216
165
  try {
217
166
  let userPrompt = params?.prompt ?? content;
218
- const { converseUsername: newConverseUsername, ensDomain: newEnsDomain } =
219
- await getUserInfo(
220
- sender.address,
221
- ensDomain[sender.address],
222
- converseUsername[sender.address],
223
- );
224
-
225
- ensDomain[sender.address] = newEnsDomain;
226
- converseUsername[sender.address] = newConverseUsername;
227
- let txUrl = `${baseTxUrl}/transaction/?transaction_type=send&buttonName=Tip%20${tipDomain[sender.address]}&amount=1&token=USDC&receiver=${tipAddress[sender.address]}`;
167
+ const userInfo = await getUserInfo(sender.address);
168
+ if (!userInfo) {
169
+ console.log("User info not found");
170
+ return;
171
+ }
172
+ const { ensDomain, converseUsername } = userInfo;
228
173
 
229
- const { reply, history } = await textGeneration(
174
+ const { reply } = await textGeneration(
175
+ sender.address,
230
176
  userPrompt,
231
- await ens_agent_prompt(
232
- sender.address,
233
- ensDomain[sender.address],
234
- converseUsername[sender.address],
235
- tipAddress[sender.address],
236
- txUrl,
237
- ),
238
- chatHistories[sender.address],
177
+ await ens_agent_prompt(sender.address, ensDomain, converseUsername),
178
+ group !== undefined,
239
179
  );
240
- if (!group) chatHistories[sender.address] = history; // Update chat history for the user
241
-
242
- await processResponseWithIntent(reply, context, sender.address);
180
+ await processResponseWithSkill(sender.address, reply, context);
243
181
  } catch (error) {
244
182
  console.error("Error during OpenAI call:", error);
245
183
  await context.send("An error occurred while processing your request.");
246
184
  }
247
185
  }
186
+
187
+ export const generateCoolAlternatives = (domain: string) => {
188
+ const suffixes = ["lfg", "cool", "degen", "moon", "base", "gm"];
189
+ const alternatives = [];
190
+ for (let i = 0; i < 5; i++) {
191
+ const randomPosition = Math.random() < 0.5;
192
+ const baseDomain = domain.replace(/\.eth$/, ""); // Remove any existing .eth suffix
193
+ alternatives.push(
194
+ randomPosition
195
+ ? `${suffixes[i]}${baseDomain}.eth`
196
+ : `${baseDomain}${suffixes[i]}.eth`,
197
+ );
198
+ }
199
+
200
+ const cool_alternativesFormat = alternatives
201
+ .map(
202
+ (alternative: string, index: number) => `${index + 1}. ${alternative} ✨`,
203
+ )
204
+ .join("\n");
205
+ return cool_alternativesFormat;
206
+ };
207
+
208
+ export async function clear() {
209
+ clearChatHistories();
210
+ clearInfoCache();
211
+ }
@@ -1,6 +1,14 @@
1
1
  import { run, HandlerContext } from "@xmtp/message-kit";
2
2
  import { ensAgent } from "./handler/ens.js";
3
3
 
4
+ export const frameUrl = "https://ens.steer.fun/";
5
+ export const ensUrl = "https://app.ens.domains/";
6
+ export const baseTxUrl = "https://base-tx-frame.vercel.app";
7
+
4
8
  run(async (context: HandlerContext) => {
9
+ const { group, message } = context;
10
+ /*All the commands are handled through the commands file*/
11
+ /* If its just text, it will be handled by the ensAgent*/
12
+ /* If its a group message, it will be handled by the groupAgent*/
5
13
  await ensAgent(context);
6
14
  });
@@ -6,12 +6,17 @@ const openai = new OpenAI({
6
6
  apiKey: process.env.OPEN_AI_API_KEY,
7
7
  });
8
8
 
9
+ export type ChatHistoryEntry = { role: string; content: string };
10
+ export type ChatHistories = Record<string, ChatHistoryEntry[]>;
11
+
12
+ let chatHistories: ChatHistories = {};
9
13
  export async function textGeneration(
14
+ address: string,
10
15
  userPrompt: string,
11
16
  systemPrompt: string,
12
- chatHistory?: any[],
17
+ isGroup: boolean = false,
13
18
  ) {
14
- let messages = chatHistory ? [...chatHistory] : []; // Start with existing chat history
19
+ let messages = chatHistories[address] || [];
15
20
  if (messages.length === 0) {
16
21
  messages.push({
17
22
  role: "system",
@@ -33,7 +38,7 @@ export async function textGeneration(
33
38
  content: reply || "No response from OpenAI.",
34
39
  });
35
40
  const cleanedReply = responseParser(reply as string);
36
-
41
+ if (!isGroup) chatHistories[address] = messages;
37
42
  return { reply: cleanedReply, history: messages };
38
43
  } catch (error) {
39
44
  console.error("Failed to fetch from OpenAI:", error);
@@ -78,6 +83,35 @@ export async function vision(imageData: Uint8Array, systemPrompt: string) {
78
83
  }
79
84
  }
80
85
 
86
+ export async function processResponseWithSkill(
87
+ address: string,
88
+ reply: string,
89
+ context: any,
90
+ ) {
91
+ let messages = reply
92
+ .split("\n")
93
+ .map((message: string) => responseParser(message))
94
+ .filter((message): message is string => message.length > 0);
95
+
96
+ console.log(messages);
97
+ for (const message of messages) {
98
+ if (message.startsWith("/")) {
99
+ const response = await context.skill(message);
100
+ if (response && response.message) {
101
+ let msg = responseParser(response.message);
102
+
103
+ chatHistories[address].push({
104
+ role: "system",
105
+ content: msg,
106
+ });
107
+
108
+ await context.send(response.message);
109
+ }
110
+ } else {
111
+ await context.send(message);
112
+ }
113
+ }
114
+ }
81
115
  export function responseParser(message: string) {
82
116
  let trimmedMessage = message;
83
117
  // Remove bold and underline markdown
@@ -94,32 +128,10 @@ export function responseParser(message: string) {
94
128
  trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
95
129
  // Remove any remaining leading or trailing whitespace
96
130
  trimmedMessage = trimmedMessage.trim();
97
- return trimmedMessage;
98
- }
99
-
100
- // UNTESTED, recursive response parser
101
- export function responseParser2(message: string | string[]): string | string[] {
102
- // If message is an array, process each item individually
103
- if (Array.isArray(message)) {
104
- return message
105
- .map((item) => responseParser(item))
106
- .flat() // Flatten nested arrays
107
- .filter((item: string) => item.length > 0)
108
- .filter((item: string) => item !== "`");
109
- }
110
- let trimmedMessage = message;
111
- // Remove bold and underline markdown
112
- trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
113
- // Remove markdown links, keeping only the URL
114
- trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
115
- // Remove markdown headers
116
- trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
117
- // Remove inline code formatting
118
- trimmedMessage = trimmedMessage?.replace(/(`{1,3})(.*?)\1/g, "$2");
119
- // Remove leading and trailing whitespace
120
- trimmedMessage = trimmedMessage?.replace(/`/g, ""); // Remove single backticks
121
- // Remove any remaining leading or trailing whitespace
122
- trimmedMessage = trimmedMessage?.trim();
123
131
 
124
132
  return trimmedMessage;
125
133
  }
134
+
135
+ export const clearChatHistories = () => {
136
+ chatHistories = {};
137
+ };
@@ -1,76 +1,120 @@
1
- import type { EnsData } from "./types.js";
2
- import { endpointURL } from "./types.js";
3
1
  import { Client } from "@xmtp/xmtp-js";
4
- import { InfoCache } from "./types.js";
2
+ import { isAddress } from "viem";
5
3
 
6
- export async function getUserInfo(
7
- address: string,
8
- ensDomain: string | undefined,
9
- converseUsername: string | undefined,
10
- ) {
11
- if (!ensDomain) {
12
- const response = await fetch(`https://ensdata.net/${address}`);
13
- const data: EnsData = (await response.json()) as EnsData;
14
- ensDomain = data?.ens;
4
+ export const converseEndpointURL =
5
+ "https://converse-website-git-endpoit-ephemerahq.vercel.app";
6
+ //export const converseEndpointURL = "http://localhost:3000";
7
+
8
+ export type InfoCache = Map<string, UserInfo>;
9
+ export type ConverseProfile = {
10
+ address: string | null;
11
+ onXmtp: boolean;
12
+ avatar: string | null;
13
+ formattedName: string | null;
14
+ name: string | null;
15
+ };
16
+ export type UserInfo = {
17
+ ensDomain?: string | undefined;
18
+ address?: string | undefined;
19
+ converseUsername?: string | undefined;
20
+ ensInfo?: EnsData | undefined;
21
+ avatar?: string | undefined;
22
+ };
23
+ export interface EnsData {
24
+ address?: string;
25
+ avatar?: string;
26
+ avatar_small?: string;
27
+ converse?: string;
28
+ avatar_url?: string;
29
+ contentHash?: string;
30
+ description?: string;
31
+ ens?: string;
32
+ ens_primary?: string;
33
+ github?: string;
34
+ resolverAddress?: string;
35
+ twitter?: string;
36
+ url?: string;
37
+ wallets?: {
38
+ eth?: string;
39
+ };
40
+ }
41
+
42
+ let infoCache: InfoCache = new Map();
43
+
44
+ export const clearInfoCache = () => {
45
+ infoCache.clear();
46
+ };
47
+ export const getUserInfo = async (
48
+ key: string,
49
+ clientAddress?: string,
50
+ ): Promise<UserInfo | null> => {
51
+ let data: UserInfo = infoCache.get(key) || {
52
+ ensDomain: undefined,
53
+ address: undefined,
54
+ converseUsername: undefined,
55
+ ensInfo: undefined,
56
+ };
57
+ //console.log("Getting user info", key, clientAddress);
58
+ if (isAddress(clientAddress || "")) {
59
+ data.address = clientAddress;
60
+ } else if (isAddress(key || "")) {
61
+ data.address = key;
62
+ } else if (key.includes(".eth")) {
63
+ data.ensDomain = key;
64
+ } else if (key == "@user" || key == "@me" || key == "@bot") {
65
+ data.address = clientAddress;
66
+ data.ensDomain = key.replace("@", "") + ".eth";
67
+ data.converseUsername = key.replace("@", "");
68
+ } else if (key == "@alix") {
69
+ data.address = "0x3a044b218BaE80E5b9E16609443A192129A67BeA";
70
+ data.converseUsername = "alix";
71
+ } else if (key == "@bo") {
72
+ data.address = "0xbc3246461ab5e1682baE48fa95172CDf0689201a";
73
+ data.converseUsername = "bo";
74
+ } else {
75
+ data.converseUsername = key;
76
+ }
77
+
78
+ let keyToUse = data.address || data.ensDomain || data.converseUsername;
79
+ let cacheData = keyToUse && infoCache.get(keyToUse);
80
+ if (cacheData) {
81
+ //console.log("Getting user info", keyToUse, cacheData);
82
+ return cacheData;
83
+ } else {
84
+ //console.log("Getting user info", keyToUse, data);
15
85
  }
16
- if (!converseUsername) {
17
- const response = await fetch(`${endpointURL}/profile/${address}`, {
86
+
87
+ if (keyToUse?.includes(".eth")) {
88
+ const response = await fetch(`https://ensdata.net/${keyToUse}`);
89
+ const ensData: EnsData = (await response.json()) as EnsData;
90
+ //console.log("Ens data", ensData);
91
+ if (ensData) {
92
+ data.ensInfo = ensData;
93
+ data.ensDomain = ensData?.ens;
94
+ data.address = ensData?.address;
95
+ }
96
+ } else if (keyToUse) {
97
+ keyToUse = keyToUse.replace("@", "");
98
+ const response = await fetch(`${converseEndpointURL}/profile/${keyToUse}`, {
18
99
  method: "POST",
19
100
  headers: {
20
101
  "Content-Type": "application/json",
21
102
  Accept: "application/json",
22
103
  },
23
- body: JSON.stringify({ address: address }),
104
+ body: JSON.stringify({
105
+ peer: keyToUse,
106
+ }),
24
107
  });
25
- const data = (await response.json()) as { name: string };
26
- converseUsername = data?.name;
27
- converseUsername = converseUsername.replace(".converse.xyz", "");
108
+ const converseData = (await response.json()) as ConverseProfile;
109
+ if (process.env.MSG_LOG)
110
+ console.log("Converse data", keyToUse, converseData);
111
+ data.converseUsername =
112
+ converseData?.formattedName || converseData?.name || undefined;
113
+ data.address = converseData?.address || undefined;
114
+ data.avatar = converseData?.avatar || undefined;
28
115
  }
29
- console.log("User info fetched for", {
30
- address,
31
- converseUsername,
32
- ensDomain,
33
- });
34
- return { converseUsername: converseUsername, ensDomain: ensDomain };
35
- }
36
-
37
- export const getInfoCache = async (
38
- key: string, // This can be either domain or address
39
- infoCache: InfoCache,
40
- ): Promise<{ domain: string; info: EnsData; infoCache: InfoCache }> => {
41
- if (infoCache[key]) {
42
- let data = {
43
- domain: key,
44
- info: infoCache[key].info,
45
- infoCache: infoCache,
46
- };
47
- return data;
48
- }
49
- try {
50
- const response = await fetch(`https://ensdata.net/${key}`);
51
- const data: EnsData = (await response.json()) as EnsData;
52
-
53
- // Assuming the data contains both domain and address
54
- const domain = data?.ens;
55
- const address = data?.address;
56
-
57
- // Store data in cache by both domain and address
58
- if (domain) infoCache[domain as string] = { info: data };
59
- if (address) infoCache[address as string] = { info: data };
60
-
61
- return { info: data, infoCache: infoCache, domain: domain as string };
62
- } catch (error) {
63
- console.error(error);
64
- return { info: {}, infoCache: infoCache, domain: "" };
65
- }
66
- };
67
- export const generateCoolAlternatives = (domain: string) => {
68
- const suffixes = ["lfg", "cool", "degen", "moon", "base", "gm"];
69
- const alternatives = suffixes.map((suffix) => {
70
- const randomPosition = Math.random() < 0.5;
71
- return randomPosition ? `${suffix}${domain}.eth` : `${domain}${suffix}.eth`;
72
- });
73
- return alternatives.join(",");
116
+ if (data.address) infoCache.set(data.address, data);
117
+ return data;
74
118
  };
75
119
  export const isOnXMTP = async (
76
120
  client: Client,