create-message-kit 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/index.js +12 -3
  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
 
@@ -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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-message-kit",
3
- "version": "1.0.15",
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