create-message-kit 1.0.14 → 1.0.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. package/index.js +16 -7
  2. package/package.json +2 -2
  3. package/templates/agent/.env.example +2 -0
  4. package/{examples/one-to-one → templates/agent}/package.json +2 -4
  5. package/templates/agent/src/commands.ts +69 -0
  6. package/templates/agent/src/handler/ens.ts +247 -0
  7. package/templates/agent/src/index.ts +6 -0
  8. package/templates/agent/src/lib/openai.ts +125 -0
  9. package/templates/agent/src/lib/resolver.ts +82 -0
  10. package/templates/agent/src/lib/types.ts +33 -0
  11. package/templates/agent/src/prompt.ts +93 -0
  12. package/templates/gm/.env.example +1 -0
  13. package/{examples → templates}/gm/package.json +1 -0
  14. package/templates/gm/src/commands.ts +18 -0
  15. package/templates/gm/src/handler.ts +6 -0
  16. package/templates/gm/src/index.ts +16 -0
  17. package/templates/group/.env.example +3 -0
  18. package/{examples → templates}/group/package.json +1 -0
  19. package/{examples → templates}/group/src/commands.ts +13 -16
  20. package/{examples → templates}/group/src/handler/agent.ts +2 -1
  21. package/{examples → templates}/group/src/handler/game.ts +2 -2
  22. package/{examples → templates}/group/src/handler/splitpayment.ts +1 -1
  23. package/{examples → templates}/group/src/handler/transaction.ts +1 -1
  24. package/{examples → templates}/group/src/index.ts +2 -17
  25. package/{examples → templates}/group/src/lib/openai.ts +0 -1
  26. package/templates/group/src/lib/resolver.ts +0 -0
  27. package/examples/gm/.env.example +0 -1
  28. package/examples/gm/src/index.ts +0 -9
  29. package/examples/group/.env.example +0 -3
  30. package/examples/one-to-one/.env.example +0 -2
  31. package/examples/one-to-one/src/index.ts +0 -72
  32. package/examples/one-to-one/src/lib/cron.ts +0 -34
  33. package/examples/one-to-one/src/lib/redis.ts +0 -15
  34. /package/{examples → templates}/group/src/handler/loyalty.ts +0 -0
  35. /package/{examples → templates}/group/src/handler/tipping.ts +0 -0
  36. /package/{examples → templates}/group/src/lib/stack.ts +0 -0
package/index.js CHANGED
@@ -19,7 +19,16 @@ program
19
19
  .name("byob")
20
20
  .description("CLI to initialize projects")
21
21
  .action(async () => {
22
- intro(pc.red(`Welcome to MessageKit CLI v${version}!`));
22
+ log.message(`\x1b[38;2;250;105;119m\
23
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24
+
25
+ ███╗ ███╗███████╗███████╗███████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗
26
+ ████╗ ████║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██╔╝██║╚══██╔══╝
27
+ ██╔████╔██║█████╗ ███████╗███████╗███████║██║ ███╗█████╗ █████╔╝ ██║ ██║
28
+ ██║╚██╔╝██║██╔══╝ ╚════██║╚════██║██╔══██║██║ ██║██╔══╝ ██╔═██╗ ██║ ██║
29
+ ██║ ╚═╝ ██║███████╗███████║███████║██║ ██║╚██████╔╝███████╗██║ ██╗██║ ██║
30
+ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
31
+ Powered by XMTP \x1b[0m`);
23
32
 
24
33
  const { templateType, displayName, destDir } = await gatherProjectInfo();
25
34
 
@@ -47,7 +56,7 @@ program
47
56
  createReadme(destDir, templateType, displayName, pkgManager);
48
57
 
49
58
  // Log next steps
50
- logNextSteps(displayName, pkgManager);
59
+ logNextSteps(displayName);
51
60
 
52
61
  outro(pc.red("Made with ❤️ by Ephemera"));
53
62
  });
@@ -57,7 +66,7 @@ program.parse(process.argv);
57
66
  async function gatherProjectInfo() {
58
67
  const templateOptions = [
59
68
  { value: "gm", label: "GM" },
60
- { value: "one-to-one", label: "One-to-One" },
69
+ { value: "agent", label: "Agent" },
61
70
  { value: "group", label: "Group" },
62
71
  ];
63
72
 
@@ -70,7 +79,7 @@ async function gatherProjectInfo() {
70
79
  process.exit(0);
71
80
  }
72
81
 
73
- const templateDir = resolve(__dirname, `./examples/${templateType}`);
82
+ const templateDir = resolve(__dirname, `./templates/${templateType}`);
74
83
 
75
84
  // Ensure the template directory exists
76
85
  if (!fs.existsSync(templateDir)) {
@@ -159,7 +168,7 @@ function updatePackageJson(destDir, name) {
159
168
  fs.writeJsonSync(resolve(destDir, "package.json"), pkgJson, { spaces: 2 });
160
169
  }
161
170
 
162
- function logNextSteps(name, pkgManager) {
171
+ function logNextSteps(name) {
163
172
  log.message("Next steps:");
164
173
  log.step(`1. ${pc.red(`cd ./${name}`)} - Navigate to project`);
165
174
  log.step(`2. ${pc.red(`code .`)} - Open with your favorite editor`);
@@ -224,7 +233,7 @@ function createReadme(destDir, templateType, projectName, packageManager) {
224
233
 
225
234
  const readmeContent = `# ${projectName}
226
235
 
227
- This project is powered by the [MessageKit](https://messagekit.ephemerahq.com/)
236
+ This project is powered by [MessageKit](https://messagekit.ephemerahq.com/)
228
237
 
229
238
  ## Setup
230
239
 
@@ -256,7 +265,7 @@ ${packageManager === "npm" ? "npm run" : packageManager} dev
256
265
 
257
266
  5. Enjoy!
258
267
  ---
259
- Made with ❤️ by [Ephemera](https://ephemerahq.com)
268
+ Made with ❤️ by [XMTP](https://xmtp.org)
260
269
  `;
261
270
 
262
271
  fs.writeFileSync(resolve(destDir, "README.md"), readmeContent.trim());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-message-kit",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -8,7 +8,7 @@
8
8
  "bin": "index.js",
9
9
  "files": [
10
10
  "index.js",
11
- "examples/**/*"
11
+ "templates/**/*"
12
12
  ],
13
13
  "scripts": {
14
14
  "clean": "rm -rf .turbo && rm -rf node_modules",
@@ -0,0 +1,2 @@
1
+ KEY= # the private key of the wallet
2
+ OPEN_AI_API_KEY= # sk-proj-...
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "one-to-one",
2
+ "name": "agent",
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
@@ -8,13 +8,11 @@
8
8
  "start": "node dist/index.js"
9
9
  },
10
10
  "dependencies": {
11
- "@redis/client": "^1.5.16",
12
11
  "@xmtp/message-kit": "workspace:*",
13
- "node-cron": "^3.0.3"
12
+ "openai": "^4.65.0"
14
13
  },
15
14
  "devDependencies": {
16
15
  "@types/node": "^20.14.2",
17
- "@types/node-cron": "^3.0.11",
18
16
  "nodemon": "^3.1.3",
19
17
  "typescript": "^5.4.5"
20
18
  },
@@ -0,0 +1,69 @@
1
+ import type { CommandGroup } from "@xmtp/message-kit";
2
+ import { ensAgent, handleEns } from "./handler/ens.js";
3
+
4
+ export const commands: CommandGroup[] = [
5
+ {
6
+ name: "Ens Domain Bot",
7
+ description: "Register ENS domains.",
8
+ commands: [
9
+ {
10
+ command: "/register [domain]",
11
+ triggers: ["/register", "@ensbot"],
12
+ handler: handleEns,
13
+ description: "Register a domain.",
14
+ params: {
15
+ domain: {
16
+ type: "string",
17
+ },
18
+ },
19
+ },
20
+ {
21
+ command: "/info [domain]",
22
+ triggers: ["/info", "@ensbot"],
23
+ handler: handleEns,
24
+ description: "Get information about a domain.",
25
+ params: {
26
+ domain: {
27
+ type: "string",
28
+ },
29
+ },
30
+ },
31
+ {
32
+ command: "/renew [domain]",
33
+ triggers: ["/renew", "@ensbot"],
34
+ handler: handleEns,
35
+ description: "Renew a domain.",
36
+ params: {
37
+ domain: {
38
+ type: "string",
39
+ },
40
+ },
41
+ },
42
+ {
43
+ command: "/check [domain] [cool_alternatives]",
44
+ triggers: ["/check"],
45
+ handler: handleEns,
46
+ description: "Check if a domain is available.",
47
+ params: {
48
+ domain: {
49
+ type: "string",
50
+ },
51
+ cool_alternatives: {
52
+ type: "quoted",
53
+ },
54
+ },
55
+ },
56
+ {
57
+ command: "/tip [address]",
58
+ description: "Show a URL for tipping a domain owner.",
59
+ triggers: ["/tip"],
60
+ handler: ensAgent,
61
+ params: {
62
+ address: {
63
+ type: "address",
64
+ },
65
+ },
66
+ },
67
+ ],
68
+ },
69
+ ];
@@ -0,0 +1,247 @@
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";
5
+ 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
23
+ export async function handleEns(context: HandlerContext) {
24
+ const {
25
+ message: {
26
+ content: { command, params, sender },
27
+ },
28
+ } = context;
29
+
30
+ if (command == "renew") {
31
+ // Destructure and validate parameters for the ens command
32
+ const { domain } = params;
33
+ // Check if the user holds the domain
34
+ if (!domain) {
35
+ context.reply("Missing required parameters. Please provide domain.");
36
+ return;
37
+ }
38
+
39
+ const { infoCache: retrievedInfoCache } = await getInfoCache(
40
+ domain,
41
+ infoCache,
42
+ );
43
+ infoCache = retrievedInfoCache;
44
+ let data = infoCache[domain].info;
45
+
46
+ if (data?.address !== sender?.address) {
47
+ return {
48
+ code: 403,
49
+ message: "You do not hold this domain. Only the owner can renew it.",
50
+ };
51
+ }
52
+
53
+ // Generate URL for the ens
54
+ let url_ens = frameUrl + "frames/manage?name=" + domain;
55
+ return { code: 200, message: `${url_ens}` };
56
+ } else if (command == "register") {
57
+ // Destructure and validate parameters for the ens command
58
+ const { domain } = params;
59
+
60
+ if (!domain) {
61
+ return {
62
+ code: 400,
63
+ message: "Missing required parameters. Please provide domain.",
64
+ };
65
+ }
66
+ // Generate URL for the ens
67
+ let url_ens = ensUrl + domain;
68
+ return { code: 200, message: `${url_ens}` };
69
+ } else if (command == "info") {
70
+ const { domain } = params;
71
+
72
+ const { infoCache: retrievedInfoCache } = await getInfoCache(
73
+ domain,
74
+ infoCache,
75
+ );
76
+ infoCache = retrievedInfoCache;
77
+ let data = infoCache[domain].info;
78
+
79
+ const formattedData = {
80
+ 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,
88
+ URL: `${ensUrl}${domain}`,
89
+ };
90
+
91
+ let message = "Domain information:\n\n";
92
+ for (const [key, value] of Object.entries(formattedData)) {
93
+ if (value) {
94
+ message += `${key}: ${value}\n`;
95
+ }
96
+ }
97
+ message += `\n\nWould you like to tip the domain owner for getting there first 🤣?`;
98
+ message = message.trim();
99
+ if (await isOnXMTP(context.v2client, data?.ens, data?.address)) {
100
+ context.send(
101
+ `Ah, this domains is in XMTP, you can message it directly: https://converse.xyz/dm/${domain}`,
102
+ );
103
+ }
104
+ return { code: 200, message };
105
+ } 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");
116
+
117
+ if (!domain) {
118
+ return {
119
+ code: 400,
120
+ message: "Please provide a domain name to check.",
121
+ };
122
+ }
123
+ const { infoCache: retrievedInfoCache } = await getInfoCache(
124
+ domain,
125
+ infoCache,
126
+ );
127
+ infoCache = retrievedInfoCache;
128
+ let data = infoCache?.[domain]?.info;
129
+ if (!data?.address) {
130
+ let message = `Looks like ${domain} is available! Do you want to register it? ${ensUrl}${domain}`;
131
+ return {
132
+ code: 200,
133
+ message,
134
+ };
135
+ } else {
136
+ let message = `Looks like ${domain} is already registered! What about these cool alternatives?\n\n${cool_alternativesFormat}`;
137
+ return {
138
+ code: 404,
139
+ message,
140
+ };
141
+ }
142
+ } else if (command == "tip") {
143
+ // Destructure and validate parameters for the send command
144
+ 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;
159
+ }
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}`);
163
+ } 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
+ }
199
+ }
200
+ }
201
+
202
+ export async function ensAgent(context: HandlerContext) {
203
+ if (!process?.env?.OPEN_AI_API_KEY) {
204
+ console.warn("No OPEN_AI_API_KEY found in .env");
205
+ return;
206
+ }
207
+
208
+ const {
209
+ message: {
210
+ content: { content, params },
211
+ sender,
212
+ },
213
+ group,
214
+ } = context;
215
+
216
+ try {
217
+ 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]}`;
228
+
229
+ const { reply, history } = await textGeneration(
230
+ 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],
239
+ );
240
+ if (!group) chatHistories[sender.address] = history; // Update chat history for the user
241
+
242
+ await processResponseWithIntent(reply, context, sender.address);
243
+ } catch (error) {
244
+ console.error("Error during OpenAI call:", error);
245
+ await context.send("An error occurred while processing your request.");
246
+ }
247
+ }
@@ -0,0 +1,6 @@
1
+ import { run, HandlerContext } from "@xmtp/message-kit";
2
+ import { ensAgent } from "./handler/ens.js";
3
+
4
+ run(async (context: HandlerContext) => {
5
+ await ensAgent(context);
6
+ });
@@ -0,0 +1,125 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+
4
+ import OpenAI from "openai";
5
+ const openai = new OpenAI({
6
+ apiKey: process.env.OPEN_AI_API_KEY,
7
+ });
8
+
9
+ export async function textGeneration(
10
+ userPrompt: string,
11
+ systemPrompt: string,
12
+ chatHistory?: any[],
13
+ ) {
14
+ let messages = chatHistory ? [...chatHistory] : []; // Start with existing chat history
15
+ if (messages.length === 0) {
16
+ messages.push({
17
+ role: "system",
18
+ content: systemPrompt,
19
+ });
20
+ }
21
+ messages.push({
22
+ role: "user",
23
+ content: userPrompt,
24
+ });
25
+ try {
26
+ const response = await openai.chat.completions.create({
27
+ model: "gpt-4o",
28
+ messages: messages as any,
29
+ });
30
+ const reply = response.choices[0].message.content;
31
+ messages.push({
32
+ role: "assistant",
33
+ content: reply || "No response from OpenAI.",
34
+ });
35
+ const cleanedReply = responseParser(reply as string);
36
+
37
+ return { reply: cleanedReply, history: messages };
38
+ } catch (error) {
39
+ console.error("Failed to fetch from OpenAI:", error);
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ // New method to interpret an image
45
+ export async function vision(imageData: Uint8Array, systemPrompt: string) {
46
+ const base64Image = Buffer.from(imageData).toString("base64");
47
+ const dataUrl = `data:image/jpeg;base64,${base64Image}`;
48
+
49
+ // Create a new thread for each vision request
50
+ const visionMessages = [
51
+ {
52
+ role: "system",
53
+ content: systemPrompt,
54
+ },
55
+ {
56
+ role: "user",
57
+ content: [
58
+ { type: "text", text: systemPrompt },
59
+ {
60
+ type: "image_url",
61
+ image_url: {
62
+ url: dataUrl,
63
+ },
64
+ },
65
+ ],
66
+ },
67
+ ];
68
+
69
+ try {
70
+ const response = await openai.chat.completions.create({
71
+ model: "gpt-4o",
72
+ messages: visionMessages as any,
73
+ });
74
+ return response.choices[0].message.content;
75
+ } catch (error) {
76
+ console.error("Failed to interpret image with OpenAI:", error);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ export function responseParser(message: string) {
82
+ let trimmedMessage = message;
83
+ // Remove bold and underline markdown
84
+ trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
85
+ // Remove markdown links, keeping only the URL
86
+ trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
87
+ // Remove markdown headers
88
+ trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
89
+ // Remove inline code formatting
90
+ trimmedMessage = trimmedMessage?.replace(/`([^`]+)`/g, "$1");
91
+ // Remove single backticks at the start or end of the message
92
+ trimmedMessage = trimmedMessage?.replace(/^`|`$/g, "");
93
+ // Remove leading and trailing whitespace
94
+ trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
95
+ // Remove any remaining leading or trailing whitespace
96
+ 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
+
124
+ return trimmedMessage;
125
+ }
@@ -0,0 +1,82 @@
1
+ import type { EnsData } from "./types.js";
2
+ import { endpointURL } from "./types.js";
3
+ import { Client } from "@xmtp/xmtp-js";
4
+ import { InfoCache } from "./types.js";
5
+
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;
15
+ }
16
+ if (!converseUsername) {
17
+ const response = await fetch(`${endpointURL}/profile/${address}`, {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ Accept: "application/json",
22
+ },
23
+ body: JSON.stringify({ address: address }),
24
+ });
25
+ const data = (await response.json()) as { name: string };
26
+ converseUsername = data?.name;
27
+ converseUsername = converseUsername.replace(".converse.xyz", "");
28
+ }
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(",");
74
+ };
75
+ export const isOnXMTP = async (
76
+ client: Client,
77
+ domain: string | undefined,
78
+ address: string | undefined,
79
+ ) => {
80
+ if (domain == "fabri.eth") return false;
81
+ if (address) return (await client.canMessage([address])).length > 0;
82
+ };
@@ -0,0 +1,33 @@
1
+ export type chatHistories = Record<string, any[]>;
2
+ export type ensDomain = Record<string, string | undefined>;
3
+ export type converseUsername = Record<string, string | undefined>;
4
+ export type tipAddress = Record<string, string | undefined>;
5
+ export type tipDomain = Record<string, string | undefined>;
6
+
7
+ export type InfoCache = {
8
+ [key: string]: { info: EnsData };
9
+ };
10
+ export const frameUrl = "https://ens.steer.fun/";
11
+ export const ensUrl = "https://app.ens.domains/";
12
+ export const baseTxUrl = "https://base-tx-frame.vercel.app";
13
+ export const endpointURL =
14
+ "https://converse-website-git-endpoit-ephemerahq.vercel.app";
15
+
16
+ export interface EnsData {
17
+ address?: string;
18
+ avatar?: string;
19
+ avatar_small?: string;
20
+ converse?: string;
21
+ avatar_url?: string;
22
+ contentHash?: string;
23
+ description?: string;
24
+ ens?: string;
25
+ ens_primary?: string;
26
+ github?: string;
27
+ resolverAddress?: string;
28
+ twitter?: string;
29
+ url?: string;
30
+ wallets?: {
31
+ eth?: string;
32
+ };
33
+ }
@@ -0,0 +1,93 @@
1
+ import { generateCoolAlternatives } from "./lib/resolver.js";
2
+ export async function ens_agent_prompt(
3
+ address: string,
4
+ domain?: string,
5
+ name?: string,
6
+ converseUsername?: string,
7
+ tipAddress?: string,
8
+ txUrl?: string,
9
+ ) {
10
+ const userName = domain ?? name ?? "";
11
+ const commonAlternatives = generateCoolAlternatives(userName);
12
+ const systemPrompt = `You are a helpful and playful agent that lives inside a web3 messaging app.
13
+ - You can respond with multiple messages if needed. Each message should be separated by a newline character.
14
+ - You can trigger commands by only sending the command in a newline message.
15
+ - Never announce actions without using a command separated by a newline character.
16
+ - Only provide answers based on verified information.
17
+ - Do not make guesses or assumptions
18
+ - CHECK that you are not missing a command
19
+
20
+ User context:
21
+ - Users address is: ${address}
22
+ ${domain != undefined ? `- User ENS domain is: ${domain}` : "- User ENS domain: None"}
23
+ ${name != undefined ? `- User name is: ${name}` : "- User name: None"}
24
+
25
+ ## Task
26
+ - Start by fetch their domain from or Convese username
27
+ - Call the user by their name or domain, in case they have one
28
+ - Ask for a name (if they don't have one) so you can suggest domains.
29
+ - Use "/check [domain] [cool_alternatives]" to see if a domain is available and offer cool alternatives
30
+ - To check the information about the domain by using the command "/info [domain]".
31
+ - To register a domain use the command "/register [domain]".
32
+ - To trigger renewal: "/renew [domain]".
33
+ - To tip the domain owner: "/tip [address]".
34
+
35
+ Commands:
36
+ - /info [domain]: Get information about a domain
37
+ - /check [domain] [cool_alternatives]: Check if a domain is available and send cool alternatives
38
+ - /register [domain]: Register a domain
39
+ - /renew [domain]: Renew a domain
40
+ - /tip [address]: Tip the domain owner
41
+
42
+ Examples:
43
+ - /check ${userName} "${commonAlternatives}"
44
+ - /info nick.eth
45
+ - /register vitalik.eth
46
+ - /renew fabri.base.eth
47
+ - /tip 0xf0EA7663233F99D0c12370671abBb6Cca980a490
48
+
49
+ ## Example response:
50
+
51
+ 1. Check if the user does not have a ENS domain
52
+ Hey ${name}! it looks like you don't have a ENS domain yet! \n\nCan you give me another name so I can suggest some cool domain alternatives for you or i can use your ${converseUsername} username? 🤔
53
+
54
+ 2. If the user has a ENS domain
55
+ Hello ${domain} ! I'll help you get your ENS domain.\n Let's start by checking your ENS domain ${domain}. Give me a moment.\n/check ${domain} "${commonAlternatives}"
56
+
57
+ 3. Check if the ENS domain is available
58
+ Hello! I'll help you get your domain.\n Let's start by checking your ENS domain ${domain}. Give me a moment.\n/check ${domain} "${commonAlternatives}"
59
+
60
+ 4. If the ENS domain is available,
61
+ Looks like ${domain} is available! Would you like to register it?\n/register ${domain}\n or I can suggest some cool alternatives? Le me know 🤔
62
+
63
+ 5. If the ENS domain is already registered, let me suggest 5 cool alternatives
64
+ Looks like ${domain} is already registered!\n What about these cool alternatives\n/check ${domain} "${commonAlternatives}"
65
+
66
+ 6. If the user wants to register a ENS domain, use the command "/register [domain]"
67
+ Looks like ${domain} is available! Let me help you register it\n/register ${domain}
68
+
69
+ 7. If the user wants to tip the ENS domain owner, use the command "/tip [address]", this will return a url to send the tip
70
+ Looks like ${domain} is already registered!\n Would you like to tip the owner for getting there first 🤣?\n/tip ${tipAddress}
71
+
72
+ 8. When sending a tip, the url will be returned in the message.
73
+ Here is the url to send the tip:\n${txUrl}
74
+
75
+ 9. If the user wants to get information about the ENS domain, use the command "/info [domain]"
76
+ Hello! I'll help you get info about ${domain}.\n Give me a moment.\n/info ${domain}
77
+
78
+ 10. If the user wants to renew their domain, use the command "/renew [domain]"
79
+ Hello! I'll help you get your ENS domain.\n Let's start by checking your ENS domain ${domain}. Give me a moment.\n/renew ${domain}
80
+
81
+ 11. If the user wants to directly to tip the ENS domain owner, use the command "/tip [address]", this will return a url to send the tip
82
+ Here is the url to send the tip:\n${txUrl}
83
+
84
+ 12. If the user wants cool suggestions about a domain, use the command "/cool [domain]"
85
+ Here are some cool suggestions for ${domain}: "${commonAlternatives}"
86
+
87
+ ## Most common bug
88
+ Some times you will say something like: "Looks like vitalik.eth is registered! What about these cool alternatives?"
89
+ But you forgot to add the command at the end of the message.
90
+ You should have said something like: "Looks like vitalik.eth is registered! What about these cool alternatives?\n/check vitalik.eth "${commonAlternatives}"
91
+ `;
92
+ return systemPrompt;
93
+ }
@@ -0,0 +1 @@
1
+ KEY= # the private key of the bot wallet
@@ -11,6 +11,7 @@
11
11
  "@xmtp/message-kit": "workspace:*"
12
12
  },
13
13
  "devDependencies": {
14
+ "@types/node": "^20.14.2",
14
15
  "nodemon": "^3.1.3",
15
16
  "typescript": "^5.4.5"
16
17
  },
@@ -0,0 +1,18 @@
1
+ import { handler as handlerAll } from "./handler.js";
2
+ import type { CommandGroup } from "@xmtp/message-kit";
3
+
4
+ export const commands: CommandGroup[] = [
5
+ {
6
+ name: "Gm Commands",
7
+ description: "Commands to send a gm.",
8
+ commands: [
9
+ {
10
+ command: "/gm",
11
+ triggers: ["/gm"],
12
+ description: "Send a gm.",
13
+ handler: handlerAll,
14
+ params: {},
15
+ },
16
+ ],
17
+ },
18
+ ];
@@ -0,0 +1,6 @@
1
+ import type { HandlerContext } from "@xmtp/message-kit";
2
+
3
+ export const handler = async (context: HandlerContext) => {
4
+ await context.send("gm2");
5
+ await context.intent("/gm3");
6
+ };
@@ -0,0 +1,16 @@
1
+ import { run, HandlerContext } from "@xmtp/message-kit";
2
+
3
+ run(
4
+ async (context: HandlerContext) => {
5
+ // Get the message and the address from the sender
6
+ const { content, sender } = context.message;
7
+
8
+ // To reply, just call `reply` on the HandlerContext
9
+ await context.send(`gm`);
10
+ },
11
+ {
12
+ memberChange: true,
13
+ attachments: true,
14
+ experimental: true,
15
+ },
16
+ );
@@ -0,0 +1,3 @@
1
+ KEY= # the private key of the bot wallet
2
+ OPEN_AI_API_KEY= # openai api key
3
+ STACK_API_KEY= # stack api key
@@ -13,6 +13,7 @@
13
13
  "openai": "^4.52.0"
14
14
  },
15
15
  "devDependencies": {
16
+ "@types/node": "^20.14.2",
16
17
  "nodemon": "^3.1.3",
17
18
  "typescript": "^5.4.5"
18
19
  },
@@ -4,15 +4,16 @@ import { handler as agent } from "./handler/agent.js";
4
4
  import { handler as transaction } from "./handler/transaction.js";
5
5
  import { handler as games } from "./handler/game.js";
6
6
  import { handler as loyalty } from "./handler/loyalty.js";
7
+ import { helpHandler } from "./index.js";
7
8
 
8
9
  export const commands: CommandGroup[] = [
9
10
  {
10
11
  name: "Tipping",
11
12
  description: "Tip tokens via emoji, replies or command.",
12
- triggers: ["/tip", "🎩", "@tip"],
13
13
  commands: [
14
14
  {
15
15
  command: "/tip [@users] [amount] [token]",
16
+ triggers: ["/tip", "🎩", "@tip"],
16
17
  description: "Tip users in a specified token.",
17
18
  handler: tipping,
18
19
  params: {
@@ -30,11 +31,11 @@ export const commands: CommandGroup[] = [
30
31
  },
31
32
  {
32
33
  name: "Transactions",
33
- triggers: ["@send", "/send", "@swap", "/swap", "/show"],
34
34
  description: "Multipurpose transaction frame built onbase.",
35
35
  commands: [
36
36
  {
37
37
  command: "/send [amount] [token] [@username]",
38
+ triggers: ["@send", "/send"],
38
39
  description:
39
40
  "Send a specified amount of a cryptocurrency to a destination address.",
40
41
  handler: transaction,
@@ -56,6 +57,7 @@ export const commands: CommandGroup[] = [
56
57
  },
57
58
  {
58
59
  command: "/swap [amount] [token_from] [token_to]",
60
+ triggers: ["@swap", "/swap"],
59
61
  description: "Exchange one type of cryptocurrency for another.",
60
62
  handler: transaction,
61
63
  params: {
@@ -77,6 +79,7 @@ export const commands: CommandGroup[] = [
77
79
  },
78
80
  {
79
81
  command: "/show",
82
+ triggers: ["/show"],
80
83
  handler: transaction,
81
84
  description: "Show the whole frame.",
82
85
  params: {},
@@ -85,11 +88,11 @@ export const commands: CommandGroup[] = [
85
88
  },
86
89
  {
87
90
  name: "Games",
88
- triggers: ["/game", "@game", "🔎", "🔍"],
89
91
  description: "Provides various gaming experiences.",
90
92
  commands: [
91
93
  {
92
94
  command: "/game [game]",
95
+ triggers: ["/game", "@game", "🔎", "🔍"],
93
96
  handler: games,
94
97
  description: "Play a game.",
95
98
  params: {
@@ -104,17 +107,19 @@ export const commands: CommandGroup[] = [
104
107
  },
105
108
  {
106
109
  name: "Loyalty",
107
- triggers: ["/points", "@points", "/leaderboard", "@leaderboard"],
108
110
  description: "Manage group members and metadata.",
109
111
  commands: [
110
112
  {
111
113
  command: "/points",
114
+ triggers: ["/points", "@points"],
112
115
  handler: loyalty,
113
116
  description: "Check your points.",
114
117
  params: {},
115
118
  },
116
119
  {
117
120
  command: "/leaderboard",
121
+ triggers: ["/leaderboard", "@leaderboard"],
122
+ adminOnly: true,
118
123
  handler: loyalty,
119
124
  description: "Check the points of a user.",
120
125
  params: {},
@@ -123,11 +128,11 @@ export const commands: CommandGroup[] = [
123
128
  },
124
129
  {
125
130
  name: "Agent",
126
- triggers: ["/agent", "@agent", "@bot"],
127
131
  description: "Manage agent commands.",
128
132
  commands: [
129
133
  {
130
134
  command: "/agent [prompt]",
135
+ triggers: ["/agent", "@agent", "@bot"],
131
136
  handler: agent,
132
137
  description: "Manage agent commands.",
133
138
  params: {
@@ -139,22 +144,14 @@ export const commands: CommandGroup[] = [
139
144
  },
140
145
  ],
141
146
  },
142
- {
143
- name: "Split Payments",
144
- image: true,
145
- triggers: [],
146
- description: "Split payments between users.",
147
- commands: [],
148
- },
149
147
  {
150
148
  name: "Help",
151
- triggers: ["/help"],
152
-
153
- description: "Get help with the bot.",
149
+ description: "Get help with the bot.",
154
150
  commands: [
155
151
  {
156
152
  command: "/help",
157
- handler: undefined,
153
+ triggers: ["/help"],
154
+ handler: helpHandler,
158
155
  description: "Get help with the bot.",
159
156
  params: {},
160
157
  },
@@ -3,7 +3,7 @@ import { textGeneration } from "../lib/openai.js";
3
3
 
4
4
  export async function handler(context: HandlerContext) {
5
5
  if (!process?.env?.OPEN_AI_API_KEY) {
6
- console.log("No OPEN_AI_API_KEY found in .env");
6
+ console.warn("No OPEN_AI_API_KEY found in .env");
7
7
  return;
8
8
  }
9
9
 
@@ -14,6 +14,7 @@ export async function handler(context: HandlerContext) {
14
14
  } = context;
15
15
 
16
16
  const systemPrompt = generateSystemPrompt(context);
17
+ console.log("systemPrompt", systemPrompt);
17
18
  try {
18
19
  let userPrompt = params?.prompt ?? content;
19
20
  console.log("userPrompt", userPrompt);
@@ -17,8 +17,8 @@ export async function handler(context: HandlerContext) {
17
17
  }
18
18
  // URLs for each game type
19
19
  const gameUrls: { [key: string]: string } = {
20
- wordle: "https://framedl.xyz/",
21
- slot: "https://slot-machine-frame.vercel.app/",
20
+ wordle: "https://framedl.xyz",
21
+ slot: "https://slot-machine-frame.vercel.app",
22
22
  };
23
23
  // Respond with the appropriate game URL or an error message
24
24
  switch (params.game) {
@@ -3,7 +3,7 @@ import { vision, textGeneration } from "../lib/openai.js";
3
3
 
4
4
  export async function handler(context: HandlerContext) {
5
5
  if (!process?.env?.OPEN_AI_API_KEY) {
6
- console.log("No OPEN_AI_API_KEY found in .env");
6
+ console.warn("No OPEN_AI_API_KEY found in .env");
7
7
  return;
8
8
  }
9
9
  const {
@@ -7,7 +7,7 @@ export async function handler(context: HandlerContext) {
7
7
  content: { command, params },
8
8
  },
9
9
  } = context;
10
- const baseUrl = "https://base-frame-lyart.vercel.app/transaction";
10
+ const baseUrl = "https://base-tx-frame.vercel.app/transaction";
11
11
 
12
12
  switch (command) {
13
13
  case "send":
@@ -15,9 +15,6 @@ run(async (context: HandlerContext) => {
15
15
  case "remoteStaticAttachment":
16
16
  handleAttachment(context);
17
17
  break;
18
- case "text":
19
- handleTextMessage(context);
20
- break;
21
18
  }
22
19
  });
23
20
  async function handleReply(context: HandlerContext) {
@@ -35,8 +32,7 @@ async function handleReply(context: HandlerContext) {
35
32
  version,
36
33
  v2client.address,
37
34
  );
38
- console.log(chain);
39
- handleTextMessage(context);
35
+ //await context.intent(chain);
40
36
  }
41
37
 
42
38
  // Handle attachment messages
@@ -44,18 +40,7 @@ async function handleAttachment(context: HandlerContext) {
44
40
  await splitpayment(context);
45
41
  }
46
42
 
47
- // Handle text messages
48
- async function handleTextMessage(context: HandlerContext) {
49
- const {
50
- content: { content: text },
51
- } = context.message;
52
- if (text.includes("/help")) {
53
- await helpHandler(context);
54
- } else if (text.startsWith("@agent")) {
55
- await agent(context);
56
- } else await context.intent(text);
57
- }
58
- async function helpHandler(context: HandlerContext) {
43
+ export async function helpHandler(context: HandlerContext) {
59
44
  const { commands = [] } = context;
60
45
  const intro =
61
46
  "Available experiences:\n" +
@@ -1,5 +1,4 @@
1
1
  import dotenv from "dotenv";
2
- import { response } from "express";
3
2
  dotenv.config();
4
3
 
5
4
  import OpenAI from "openai";
File without changes
@@ -1 +0,0 @@
1
- KEY= # 0x... the private key of the bot wallet (with the 0x prefix)
@@ -1,9 +0,0 @@
1
- import { run, HandlerContext } from "@xmtp/message-kit";
2
-
3
- run(async (context: HandlerContext) => {
4
- // Get the message and the address from the sender
5
- const { content, sender } = context.message;
6
-
7
- // To reply, just call `reply` on the HandlerContext
8
- await context.send(`gm`);
9
- });
@@ -1,3 +0,0 @@
1
- KEY= # 0x... the private key of the bot wallet (with the 0x prefix)
2
- OPEN_AI_API_KEY= # openai api key
3
- STACK_API_KEY= # stack api key
@@ -1,2 +0,0 @@
1
- KEY= # 0x... the private key of the bot wallet (with the 0x prefix)
2
- REDIS_CONNECTION_STRING= # redis db connection string
@@ -1,72 +0,0 @@
1
- import { getRedisClient } from "./lib/redis.js";
2
- import { run, HandlerContext } from "@xmtp/message-kit";
3
- import { startCron } from "./lib/cron.js";
4
- import { RedisClientType } from "@redis/client";
5
-
6
- //Tracks conversation steps
7
- const inMemoryCacheStep = new Map<string, number>();
8
-
9
- //List of words to stop or unsubscribe.
10
- const stopWords = ["stop", "unsubscribe", "cancel", "list"];
11
-
12
- const redisClient: RedisClientType = await getRedisClient();
13
-
14
- let clientInitialized = false;
15
- run(async (context: HandlerContext) => {
16
- const {
17
- v2client,
18
- message: {
19
- content: { content: text },
20
- typeId,
21
- sender,
22
- },
23
- } = context;
24
-
25
- if (!clientInitialized) {
26
- startCron(redisClient, v2client);
27
- clientInitialized = true;
28
- }
29
- if (typeId !== "text") {
30
- /* If the input is not text do nothing */
31
- return;
32
- }
33
-
34
- const lowerContent = text?.toLowerCase();
35
-
36
- //Handles unsubscribe and resets step
37
- if (stopWords.some((word) => lowerContent.includes(word))) {
38
- inMemoryCacheStep.set(sender.address, 0);
39
- await redisClient.del(sender.address);
40
- await context.reply(
41
- "You are now unsubscribed. You will no longer receive updates!.",
42
- );
43
- return;
44
- }
45
-
46
- const cacheStep = inMemoryCacheStep.get(sender.address) || 0;
47
- let message = "";
48
- if (cacheStep === 0) {
49
- message = "Welcome! Choose an option:\n1. Info\n2. Subscribe";
50
- // Move to the next step
51
- inMemoryCacheStep.set(sender.address, cacheStep + 1);
52
- } else if (cacheStep === 1) {
53
- if (text === "1") {
54
- message = "Here is the info.";
55
- } else if (text === "2") {
56
- await redisClient.set(sender.address, "subscribed"); //test
57
- message =
58
- "You are now subscribed. You will receive updates.\n\ntype 'stop' to unsubscribe";
59
- //reset the app to the initial step
60
- inMemoryCacheStep.set(sender.address, 0);
61
- } else {
62
- message = "Invalid option. Please choose 1 for Info or 2 to Subscribe.";
63
- // Keep the same step to allow for re-entry
64
- }
65
- } else {
66
- message = "Invalid option. Please start again.";
67
- inMemoryCacheStep.set(sender.address, 0);
68
- }
69
-
70
- //Send the message
71
- await context.reply(message);
72
- });
@@ -1,34 +0,0 @@
1
- import cron from "node-cron";
2
- import { Client } from "@xmtp/xmtp-js";
3
- import { RedisClientType } from "@redis/client";
4
-
5
- export async function startCron(
6
- redisClient: RedisClientType,
7
- v2client: Client,
8
- ) {
9
- console.log("Starting daily cron");
10
- const conversations = await v2client.conversations.list();
11
- cron.schedule(
12
- "0 0 * * *", // Daily or every 5 seconds in debug mode
13
- async () => {
14
- const keys = await redisClient.keys("*");
15
- console.log(`Running daily task. ${keys.length} subscribers.`);
16
- for (const address of keys) {
17
- const subscriptionStatus = await redisClient.get(address);
18
- if (subscriptionStatus === "subscribed") {
19
- console.log(`Sending daily update to ${address}`);
20
- // Logic to send daily updates to each subscriber
21
- const targetConversation = conversations.find(
22
- (conv) => conv.peerAddress === address,
23
- );
24
- if (targetConversation)
25
- await targetConversation.send("Here is your daily update!");
26
- }
27
- }
28
- },
29
- {
30
- scheduled: true,
31
- timezone: "UTC",
32
- },
33
- );
34
- }
@@ -1,15 +0,0 @@
1
- import { createClient } from "@redis/client";
2
- import { RedisClientType } from "@redis/client";
3
-
4
- export const getRedisClient = async () => {
5
- const client = createClient({
6
- url: process.env.REDIS_CONNECTION_STRING,
7
- });
8
-
9
- client.on("error", (err) => {
10
- console.error("Redis client error:", err);
11
- });
12
-
13
- await client.connect();
14
- return client as RedisClientType;
15
- };
File without changes