create-message-kit 1.1.5 → 1.1.7-beta.10

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -15,3 +15,7 @@ npx create-message-kit@latest
15
15
  ```bash
16
16
  yarn create message-kit
17
17
  ```
18
+
19
+ ```bash [npm]
20
+ npm init message-kit@latest
21
+ ```
package/index.js CHANGED
@@ -14,15 +14,11 @@ const packageJson = JSON.parse(
14
14
  fs.readFileSync(resolve(__dirname, "package.json"), "utf8"),
15
15
  );
16
16
  const version = packageJson.version;
17
- const pckMessageKitLib = JSON.parse(
18
- fs.readFileSync(resolve(__dirname, "../message-kit/package.json"), "utf8"),
19
- );
20
- const versionMessageKitLib = pckMessageKitLib.version;
21
17
  program
22
18
  .name("byob")
23
19
  .description("CLI to initialize projects")
24
20
  .action(async () => {
25
- log.info(pc.cyan(`Welcome to MessageKit v${versionMessageKitLib}!`));
21
+ log.info(pc.cyan(`Welcome to MessageKit CLI v${version}!`));
26
22
  const coolLogo = `
27
23
  ███╗ ███╗███████╗███████╗███████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗
28
24
  ████╗ ████║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██╔╝██║╚══██╔══╝
@@ -183,7 +179,6 @@ function createGitignore(destDir) {
183
179
  # Main
184
180
  .env
185
181
  node_modules/
186
- .gitignore
187
182
  .data/
188
183
  dist/
189
184
  .DS_Store
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-message-kit",
3
- "version": "1.1.5",
3
+ "version": "1.1.7-beta.10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,16 +1,19 @@
1
- import { HandlerContext } from "@xmtp/message-kit";
2
- import { getUserInfo, clearInfoCache, isOnXMTP } from "../lib/resolver.js";
1
+ import { HandlerContext, SkillResponse } from "@xmtp/message-kit";
2
+ import { getUserInfo, clearInfoCache, isOnXMTP } from "@xmtp/message-kit";
3
3
  import { isAddress } from "viem";
4
- import { clearMemory } from "../lib/openai.js";
4
+ import { clearMemory } from "@xmtp/message-kit";
5
5
 
6
6
  export const frameUrl = "https://ens.steer.fun/";
7
7
  export const ensUrl = "https://app.ens.domains/";
8
8
  export const baseTxUrl = "https://base-tx-frame.vercel.app";
9
9
 
10
- export async function handleEns(context: HandlerContext) {
10
+ export async function handleEns(
11
+ context: HandlerContext,
12
+ ): Promise<SkillResponse | undefined> {
11
13
  const {
12
14
  message: {
13
- content: { command, params, sender },
15
+ sender,
16
+ content: { command, params },
14
17
  },
15
18
  } = context;
16
19
  if (command == "reset") {
@@ -84,13 +87,7 @@ export async function handleEns(context: HandlerContext) {
84
87
  }
85
88
  message += `\n\nWould you like to tip the domain owner for getting there first 🤣?`;
86
89
  message = message.trim();
87
- if (
88
- await isOnXMTP(
89
- context.v2client,
90
- data?.ensInfo?.ens,
91
- data?.ensInfo?.address,
92
- )
93
- ) {
90
+ if (await isOnXMTP(context.client, context.v2client, sender?.address)) {
94
91
  await context.send(
95
92
  `Ah, this domains is in XMTP, you can message it directly: https://converse.xyz/dm/${domain}`,
96
93
  );
@@ -115,7 +112,7 @@ export async function handleEns(context: HandlerContext) {
115
112
  };
116
113
  } else {
117
114
  let message = `Looks like ${domain} is already registered!`;
118
- await context.skill("/cool " + domain);
115
+ await context.executeSkill("/cool " + domain);
119
116
  return {
120
117
  code: 404,
121
118
  message,
@@ -145,6 +142,8 @@ export async function handleEns(context: HandlerContext) {
145
142
  code: 200,
146
143
  message: `${generateCoolAlternatives(domain)}`,
147
144
  };
145
+ } else {
146
+ return { code: 400, message: "Command not found." };
148
147
  }
149
148
  }
150
149
 
@@ -1,10 +1,10 @@
1
1
  import { run, HandlerContext } from "@xmtp/message-kit";
2
- import { textGeneration, processResponseWithSkill } from "./lib/openai.js";
2
+ import { textGeneration, processMultilineResponse } from "@xmtp/message-kit";
3
3
  import { agent_prompt } from "./prompt.js";
4
- import { getUserInfo } from "./lib/resolver.js";
4
+ import { getUserInfo } from "@xmtp/message-kit";
5
5
 
6
6
  run(async (context: HandlerContext) => {
7
- /*All the commands are handled through the commands file*/
7
+ /*All the skills are handled through the skills file*/
8
8
  /* If its just text, it will be handled by the ensAgent*/
9
9
  /* If its a group message, it will be handled by the groupAgent*/
10
10
  if (!process?.env?.OPEN_AI_API_KEY) {
@@ -31,7 +31,7 @@ run(async (context: HandlerContext) => {
31
31
  userPrompt,
32
32
  await agent_prompt(userInfo),
33
33
  );
34
- await processResponseWithSkill(sender.address, reply, context);
34
+ await processMultilineResponse(sender.address, reply, context);
35
35
  } catch (error) {
36
36
  console.error("Error during OpenAI call:", error);
37
37
  await context.send("An error occurred while processing your request.");
@@ -0,0 +1,161 @@
1
+ import "dotenv/config";
2
+ import type { SkillGroup } from "@xmtp/message-kit";
3
+ import OpenAI from "openai";
4
+ const openai = new OpenAI({
5
+ apiKey: process.env.OPEN_AI_API_KEY,
6
+ });
7
+
8
+ type ChatHistoryEntry = { role: string; content: string };
9
+ type ChatHistories = Record<string, ChatHistoryEntry[]>;
10
+ // New ChatMemory class
11
+ class ChatMemory {
12
+ private histories: ChatHistories = {};
13
+
14
+ getHistory(address: string): ChatHistoryEntry[] {
15
+ return this.histories[address] || [];
16
+ }
17
+
18
+ addEntry(address: string, entry: ChatHistoryEntry) {
19
+ if (!this.histories[address]) {
20
+ this.histories[address] = [];
21
+ }
22
+ this.histories[address].push(entry);
23
+ }
24
+
25
+ initializeWithSystem(address: string, systemPrompt: string) {
26
+ if (this.getHistory(address).length === 0) {
27
+ this.addEntry(address, {
28
+ role: "system",
29
+ content: systemPrompt,
30
+ });
31
+ }
32
+ }
33
+
34
+ clear() {
35
+ this.histories = {};
36
+ }
37
+ }
38
+
39
+ // Create singleton instance
40
+ export const chatMemory = new ChatMemory();
41
+
42
+ export const clearMemory = () => {
43
+ chatMemory.clear();
44
+ };
45
+
46
+ export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
47
+ - You can respond with multiple messages if needed. Each message should be separated by a newline character.
48
+ - You can trigger skills by only sending the command in a newline message.
49
+ - Never announce actions without using a command separated by a newline character.
50
+ - Dont answer in markdown format, just answer in plaintext.
51
+ - Do not make guesses or assumptions
52
+ - Only answer if the verified information is in the prompt.
53
+ - Check that you are not missing a command
54
+ - Focus only on helping users with operations detailed below.
55
+ `;
56
+
57
+ export function PROMPT_SKILLS_AND_EXAMPLES(skills: SkillGroup[], tag: string) {
58
+ let foundSkills = skills.filter(
59
+ (skill) => skill.tag == `@${tag.toLowerCase()}`,
60
+ );
61
+ if (!foundSkills.length || !foundSkills[0] || !foundSkills[0].skills)
62
+ return "";
63
+ let returnPrompt = `\nCommands:\n${foundSkills[0].skills
64
+ .map((skill) => skill.command)
65
+ .join("\n")}\n\nExamples:\n${foundSkills[0].skills
66
+ .map((skill) => skill.examples)
67
+ .join("\n")}`;
68
+ return returnPrompt;
69
+ }
70
+
71
+ export async function textGeneration(
72
+ memoryKey: string,
73
+ userPrompt: string,
74
+ systemPrompt: string,
75
+ ) {
76
+ if (!memoryKey) {
77
+ clearMemory();
78
+ }
79
+ let messages = chatMemory.getHistory(memoryKey);
80
+ chatMemory.initializeWithSystem(memoryKey, systemPrompt);
81
+ if (messages.length === 0) {
82
+ messages.push({
83
+ role: "system",
84
+ content: systemPrompt,
85
+ });
86
+ }
87
+ messages.push({
88
+ role: "user",
89
+ content: userPrompt,
90
+ });
91
+ try {
92
+ const response = await openai.chat.completions.create({
93
+ model: "gpt-4o",
94
+ messages: messages as any,
95
+ });
96
+ const reply = response.choices[0].message.content;
97
+ messages.push({
98
+ role: "assistant",
99
+ content: reply || "No response from OpenAI.",
100
+ });
101
+ const cleanedReply = parseMarkdown(reply as string);
102
+ chatMemory.addEntry(memoryKey, {
103
+ role: "assistant",
104
+ content: cleanedReply,
105
+ });
106
+ return { reply: cleanedReply, history: messages };
107
+ } catch (error) {
108
+ console.error("Failed to fetch from OpenAI:", error);
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ export async function processMultilineResponse(
114
+ memoryKey: string,
115
+ reply: string,
116
+ context: any,
117
+ ) {
118
+ if (!memoryKey) {
119
+ clearMemory();
120
+ }
121
+ let messages = reply
122
+ .split("\n")
123
+ .map((message: string) => parseMarkdown(message))
124
+ .filter((message): message is string => message.length > 0);
125
+
126
+ console.log(messages);
127
+ for (const message of messages) {
128
+ if (message.startsWith("/")) {
129
+ const response = await context.executeSkill(message);
130
+ if (response && typeof response.message === "string") {
131
+ let msg = parseMarkdown(response.message);
132
+ chatMemory.addEntry(memoryKey, {
133
+ role: "system",
134
+ content: msg,
135
+ });
136
+ await context.send(response.message);
137
+ }
138
+ } else {
139
+ await context.send(message);
140
+ }
141
+ }
142
+ }
143
+ export function parseMarkdown(message: string) {
144
+ let trimmedMessage = message;
145
+ // Remove bold and underline markdown
146
+ trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
147
+ // Remove markdown links, keeping only the URL
148
+ trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
149
+ // Remove markdown headers
150
+ trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
151
+ // Remove inline code formatting
152
+ trimmedMessage = trimmedMessage?.replace(/`([^`]+)`/g, "$1");
153
+ // Remove single backticks at the start or end of the message
154
+ trimmedMessage = trimmedMessage?.replace(/^`|`$/g, "");
155
+ // Remove leading and trailing whitespace
156
+ trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
157
+ // Remove any remaining leading or trailing whitespace
158
+ trimmedMessage = trimmedMessage.trim();
159
+
160
+ return trimmedMessage;
161
+ }
@@ -48,7 +48,7 @@ export const chatMemory = new ChatMemory();
48
48
  let chatHistories: ChatHistories = {};
49
49
  export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
50
50
  - You can respond with multiple messages if needed. Each message should be separated by a newline character.
51
- - You can trigger commands by only sending the command in a newline message.
51
+ - You can trigger skills by only sending the command in a newline message.
52
52
  - Never announce actions without using a command separated by a newline character.
53
53
  - Dont answer in markdown format, just answer in plaintext.
54
54
  - Do not make guesses or assumptions
@@ -81,7 +81,7 @@ export async function agentResponse(
81
81
  userPrompt,
82
82
  systemPrompt,
83
83
  );
84
- await processResponseWithSkill(sender.address, reply, context);
84
+ await processMultilineResponse(sender.address, reply, context);
85
85
  } catch (error) {
86
86
  console.error("Error during OpenAI call:", error);
87
87
  await context.reply("An error occurred while processing your request.");
@@ -126,7 +126,7 @@ export async function textGeneration(
126
126
  }
127
127
  }
128
128
 
129
- export async function processResponseWithSkill(
129
+ export async function processMultilineResponse(
130
130
  address: string,
131
131
  reply: string,
132
132
  context: any,
@@ -1,5 +1,6 @@
1
- import { Client } from "@xmtp/xmtp-js";
1
+ import type { Client } from "@xmtp/xmtp-js";
2
2
  import { isAddress } from "viem";
3
+ import type { HandlerContext } from "@xmtp/message-kit";
3
4
 
4
5
  export const converseEndpointURL =
5
6
  "https://converse-website-git-endpoit-ephemerahq.vercel.app";
@@ -16,8 +17,8 @@ export type ConverseProfile = {
16
17
  export type UserInfo = {
17
18
  ensDomain?: string | undefined;
18
19
  address?: string | undefined;
20
+ preferredName: string | undefined;
19
21
  converseUsername?: string | undefined;
20
- preferredName?: string | undefined;
21
22
  ensInfo?: EnsData | undefined;
22
23
  avatar?: string | undefined;
23
24
  };
@@ -48,12 +49,14 @@ export const clearInfoCache = () => {
48
49
  export const getUserInfo = async (
49
50
  key: string,
50
51
  clientAddress?: string,
52
+ context?: HandlerContext,
51
53
  ): Promise<UserInfo | null> => {
52
54
  let data: UserInfo = infoCache.get(key) || {
53
55
  ensDomain: undefined,
54
56
  address: undefined,
55
57
  converseUsername: undefined,
56
58
  ensInfo: undefined,
59
+ preferredName: undefined,
57
60
  };
58
61
  if (isAddress(clientAddress || "")) {
59
62
  data.address = clientAddress;
@@ -74,12 +77,15 @@ export const getUserInfo = async (
74
77
  } else {
75
78
  data.converseUsername = key;
76
79
  }
77
-
80
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
78
81
  let keyToUse = data.address || data.ensDomain || data.converseUsername;
79
82
  let cacheData = keyToUse && infoCache.get(keyToUse);
80
- console.log("Getting user info", { cacheData, keyToUse, data });
83
+ //console.log("Getting user info", { cacheData, keyToUse, data });
81
84
  if (cacheData) return cacheData;
82
85
 
86
+ context?.send(
87
+ "Hey there! Give me a sec while I fetch info about you first...",
88
+ );
83
89
  if (keyToUse?.includes(".eth")) {
84
90
  const response = await fetch(`https://ensdata.net/${keyToUse}`);
85
91
  const ensData: EnsData = (await response.json()) as EnsData;
@@ -102,13 +108,15 @@ export const getUserInfo = async (
102
108
  }),
103
109
  });
104
110
  const converseData = (await response.json()) as ConverseProfile;
105
- if (process.env.MSG_LOG)
106
- console.log("Converse data", keyToUse, converseData);
111
+ //if (process.env.MSG_LOG === "true")
112
+ //console.log("Converse data", keyToUse, converseData);
107
113
  data.converseUsername =
108
114
  converseData?.formattedName || converseData?.name || undefined;
109
115
  data.address = converseData?.address || undefined;
110
116
  data.avatar = converseData?.avatar || undefined;
111
117
  }
118
+
119
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
112
120
  if (data.address) infoCache.set(data.address, data);
113
121
  return data;
114
122
  };
@@ -123,7 +131,8 @@ export const isOnXMTP = async (
123
131
 
124
132
  export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
125
133
  let { address, ensDomain, converseUsername, preferredName } = userInfo;
126
- let prompt = `User context:
134
+ let prompt = `
135
+ User context:
127
136
  - Start by fetch their domain from or Convese username
128
137
  - Call the user by their name or domain, in case they have one
129
138
  - Ask for a name (if they don't have one) so you can suggest domains.
@@ -132,5 +141,11 @@ export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
132
141
  if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
133
142
  if (converseUsername)
134
143
  prompt += `\n- Converse username is: ${converseUsername}`;
144
+
145
+ prompt = prompt.replace("{ADDRESS}", address || "");
146
+ prompt = prompt.replace("{ENS_DOMAIN}", ensDomain || "");
147
+ prompt = prompt.replace("{CONVERSE_USERNAME}", converseUsername || "");
148
+ prompt = prompt.replace("{PREFERRED_NAME}", preferredName || "");
149
+
135
150
  return prompt;
136
151
  };
@@ -1,6 +1,11 @@
1
1
  import { skills } from "./skills.js";
2
- import { UserInfo, PROMPT_USER_CONTENT } from "./lib/resolver.js";
3
- import { PROMPT_RULES, PROMPT_SKILLS_AND_EXAMPLES } from "./lib/openai.js";
2
+ import {
3
+ getUserInfo,
4
+ UserInfo,
5
+ PROMPT_USER_CONTENT,
6
+ PROMPT_RULES,
7
+ PROMPT_SKILLS_AND_EXAMPLES,
8
+ } from "@xmtp/message-kit";
4
9
 
5
10
  export async function agent_prompt(userInfo: UserInfo) {
6
11
  let { address, ensDomain, converseUsername, preferredName } = userInfo;
@@ -12,7 +17,7 @@ export async function agent_prompt(userInfo: UserInfo) {
12
17
  systemPrompt += PROMPT_USER_CONTENT(userInfo);
13
18
 
14
19
  //Add skills and examples to the prompt
15
- systemPrompt += PROMPT_SKILLS_AND_EXAMPLES(skills);
20
+ systemPrompt += PROMPT_SKILLS_AND_EXAMPLES(skills, "@ens");
16
21
 
17
22
  systemPrompt += `
18
23
 
@@ -20,6 +20,19 @@ export const skills: SkillGroup[] = [
20
20
  },
21
21
  },
22
22
  },
23
+ {
24
+ command: "/exists",
25
+ adminOnly: true,
26
+ examples: ["/exists"],
27
+ handler: handleEns,
28
+ triggers: ["/exists"],
29
+ description: "Check if an address is onboarded.",
30
+ params: {
31
+ address: {
32
+ type: "address",
33
+ },
34
+ },
35
+ },
23
36
  {
24
37
  command: "/info [domain]",
25
38
  triggers: ["/info"],
@@ -1,5 +1,5 @@
1
1
  import { HandlerContext, AbstractedMember } from "@xmtp/message-kit";
2
- import { textGeneration } from "../lib/openai.js";
2
+ import { textGeneration } from "@xmtp/message-kit";
3
3
 
4
4
  export async function handler(context: HandlerContext) {
5
5
  if (!process?.env?.OPEN_AI_API_KEY) {
@@ -10,20 +10,26 @@ export async function handler(context: HandlerContext) {
10
10
  const {
11
11
  message: {
12
12
  sender,
13
- content: { content, params },
13
+ content: { params, text },
14
14
  },
15
15
  } = context;
16
16
 
17
17
  const systemPrompt = generateSystemPrompt(context);
18
18
  try {
19
- let userPrompt = params?.prompt ?? content;
19
+ let userPrompt = params?.prompt ?? text;
20
20
 
21
21
  const { reply } = await textGeneration(
22
22
  sender.address,
23
23
  userPrompt,
24
24
  systemPrompt,
25
25
  );
26
- context.skill(reply);
26
+
27
+ try {
28
+ await context.executeSkill(reply);
29
+ } catch (error) {
30
+ console.error("Error executing skill:", error);
31
+ await context.reply("Failed to execute the requested action.");
32
+ }
27
33
  } catch (error) {
28
34
  console.error("Error during OpenAI call:", error);
29
35
  await context.reply("An error occurred while processing your request.");
@@ -59,7 +65,7 @@ function generateSystemPrompt(context: HandlerContext) {
59
65
  Important:
60
66
  - If a user asks jokes, make jokes about web3 devs\n
61
67
  - If the user asks about performing an action and you can think of a command that would help, answer directly with the command and nothing else.
62
- - Populate the command with the correct or random values. Always return commands with real values only, using usernames with @ and excluding addresses.\n
68
+ - Populate the command with the correct or random values. Always return skills with real values only, using usernames with @ and excluding addresses.\n
63
69
  - If the user asks a question or makes a statement that does not clearly map to a command, respond with helpful information or a clarification question.\n
64
70
  - If the user is grateful, respond asking for a tip in a playful manner.
65
71
  `;
@@ -4,11 +4,10 @@ import { HandlerContext } from "@xmtp/message-kit";
4
4
  export async function handler(context: HandlerContext) {
5
5
  const {
6
6
  message: {
7
- content: { command, params },
7
+ content: { command, params, text },
8
8
  },
9
9
  } = context;
10
10
  if (!command) {
11
- const { content: text } = context?.message?.content;
12
11
  if (text === "🔎" || text === "🔍") {
13
12
  // Send the URL for the requested game
14
13
  context.reply("https://framedl.xyz/");
@@ -34,7 +33,7 @@ export async function handler(context: HandlerContext) {
34
33
  context.send("Available games: \n/game wordle\n/game slot");
35
34
  break;
36
35
  default:
37
- // Inform the user about unrecognized commands and provide available options
36
+ // Inform the user about unrecognized skills and provide available options
38
37
  context.send(
39
38
  "Command not recognized. Available games: wordle, slot, or help.",
40
39
  );
@@ -0,0 +1,24 @@
1
+ import { HandlerContext } from "@xmtp/message-kit";
2
+
3
+ export async function handler(context: HandlerContext) {
4
+ const {
5
+ skills,
6
+ group,
7
+ message: {
8
+ content: { command },
9
+ },
10
+ } = context;
11
+
12
+ if (command == "help") {
13
+ const intro =
14
+ "Available experiences:\n" +
15
+ skills
16
+ ?.flatMap((app) => app.skills)
17
+ .map((skill) => `${skill.command} - ${skill.description}`)
18
+ .join("\n") +
19
+ "\nUse these skills to interact with specific apps.";
20
+ context.send(intro);
21
+ } else if (command == "id") {
22
+ context.send(context.group?.id);
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import { HandlerContext } from "@xmtp/message-kit";
2
+
3
+ export async function handler(context: HandlerContext) {
4
+ const {
5
+ skills,
6
+ message: {
7
+ content: { command },
8
+ },
9
+ } = context;
10
+
11
+ if (command == "help") {
12
+ const intro =
13
+ "Available experiences:\n" +
14
+ skills
15
+ ?.flatMap((app) => app.skills)
16
+ .map((skill) => `${skill.command} - ${skill.description}`)
17
+ .join("\n") +
18
+ "\nUse these skills to interact with specific apps.";
19
+ context.send(intro);
20
+ } else if (command == "id") {
21
+ context.send(context.group?.id);
22
+ }
23
+ }
@@ -6,10 +6,13 @@ export async function handler(context: HandlerContext, fake?: boolean) {
6
6
  const {
7
7
  members,
8
8
  group,
9
- message: { sender, typeId, content },
9
+ message: {
10
+ sender,
11
+ typeId,
12
+ content: { command, params, text },
13
+ },
10
14
  } = context;
11
15
  if (typeId === "text" && group) {
12
- const { command } = content;
13
16
  if (command === "points") {
14
17
  const points = await stack?.getPoints(sender.address);
15
18
  context.reply(`You have ${points} points`);
@@ -31,7 +34,7 @@ export async function handler(context: HandlerContext, fake?: boolean) {
31
34
  return;
32
35
  }
33
36
  } else if (typeId === "group_updated" && group) {
34
- const { initiatedByInboxId, addedInboxes } = content;
37
+ const { initiatedByInboxId, addedInboxes } = params;
35
38
  const adminAddress = members?.find(
36
39
  (member: AbstractedMember) => member.inboxId === initiatedByInboxId,
37
40
  );
@@ -1,5 +1,5 @@
1
1
  import { HandlerContext } from "@xmtp/message-kit";
2
- import { textGeneration } from "../lib/openai.js";
2
+ import { textGeneration } from "../lib/gpt.js";
3
3
  import { vision } from "../lib/vision.js";
4
4
  import { getUserInfo } from "../lib/resolver.js";
5
5
 
@@ -10,7 +10,7 @@ export async function handler(context: HandlerContext) {
10
10
  }
11
11
  const {
12
12
  members,
13
- skills,
13
+ skill,
14
14
  message: {
15
15
  typeId,
16
16
  content: { attachment },
@@ -57,7 +57,7 @@ export async function handler(context: HandlerContext) {
57
57
  let splitMessages = JSON.parse(reply);
58
58
  for (const message of splitMessages) {
59
59
  let msg = message as string;
60
- if (msg.startsWith("/")) await context.skill(msg);
60
+ if (msg.startsWith("/")) await skill(msg);
61
61
  else await context.send(msg);
62
62
  }
63
63
  }
@@ -1,14 +1,21 @@
1
- import { HandlerContext, AbstractedMember } from "@xmtp/message-kit";
2
- import { getUserInfo } from "../lib/resolver.js";
1
+ import {
2
+ HandlerContext,
3
+ AbstractedMember,
4
+ SkillResponse,
5
+ } from "@xmtp/message-kit";
6
+ import { getUserInfo } from "@xmtp/message-kit";
3
7
 
4
- export async function handler(context: HandlerContext) {
8
+ export async function handler(context: HandlerContext): Promise<SkillResponse> {
5
9
  const {
6
10
  members,
7
11
  getMessageById,
8
- message: { content, sender, typeId },
12
+ message: {
13
+ content: { reference, reply, text, params },
14
+ sender,
15
+ typeId,
16
+ },
9
17
  } = context;
10
- console.log(sender);
11
- const msg = await getMessageById(content.reference);
18
+ const msg = reference ? await getMessageById(reference) : undefined;
12
19
  const replyReceiver = members?.find(
13
20
  (member) => member.inboxId === msg?.senderInboxId,
14
21
  );
@@ -16,34 +23,29 @@ export async function handler(context: HandlerContext) {
16
23
  receivers: AbstractedMember[] = [];
17
24
  // Handle different types of messages
18
25
  if (typeId === "reply" && replyReceiver) {
19
- const { content: reply } = content;
20
-
21
- if (reply.includes("degen")) {
26
+ if (reply?.includes("degen")) {
22
27
  receivers = [replyReceiver];
23
28
  const match = reply.match(/(\d+)/);
24
29
  if (match)
25
30
  amount = parseInt(match[0]); // Extract amount from reply
26
31
  else amount = 10;
27
32
  }
28
- } else if (typeId === "text") {
29
- const { content: text, params } = content;
30
- if (text.startsWith("/tip") && params) {
31
- // Process text commands starting with "/tip"
32
- const {
33
- params: { amount: extractedAmount, username },
34
- } = content;
35
- amount = extractedAmount || 10; // Default amount if not specified
33
+ } else if (typeId === "text" && text?.startsWith("/tip") && params) {
34
+ // Process text skills starting with "/tip"
35
+ const { amount: extractedAmount, username } = params;
36
+ amount = extractedAmount || 10; // Default amount if not specified
36
37
 
37
- receivers = await Promise.all(
38
- username.map((username: string) => getUserInfo(username)),
39
- );
40
- }
38
+ receivers = await Promise.all(
39
+ username.map((username: string) => getUserInfo(username)),
40
+ );
41
41
  }
42
42
  if (!sender || receivers.length === 0 || amount === 0) {
43
43
  context.reply("Sender or receiver or amount not found.");
44
- return;
44
+ return {
45
+ code: 400,
46
+ message: "Sender or receiver or amount not found.",
47
+ };
45
48
  }
46
- console.log(receivers);
47
49
  const receiverAddresses = receivers.map((receiver) => receiver.address);
48
50
  // Process sending tokens to each receiver
49
51
 
@@ -57,4 +59,8 @@ export async function handler(context: HandlerContext) {
57
59
  `You sent ${amount * receiverAddresses.length} tokens in total.`,
58
60
  [sender.address],
59
61
  );
62
+ return {
63
+ code: 200,
64
+ message: "Success",
65
+ };
60
66
  }
@@ -1,8 +1,7 @@
1
- import { HandlerContext } from "@xmtp/message-kit";
2
- import { getUserInfo } from "../lib/resolver.js";
1
+ import { getUserInfo, HandlerContext, SkillResponse } from "@xmtp/message-kit";
3
2
 
4
3
  // Main handler function for processing commands
5
- export async function handler(context: HandlerContext) {
4
+ export async function handler(context: HandlerContext): Promise<SkillResponse> {
6
5
  const {
7
6
  message: {
8
7
  content: { command, params },
@@ -19,13 +18,19 @@ export async function handler(context: HandlerContext) {
19
18
  context.reply(
20
19
  "Missing required parameters. Please provide amount, token, and username.",
21
20
  );
22
- return;
21
+ return {
22
+ code: 400,
23
+ message:
24
+ "Missing required parameters. Please provide amount, token, and username.",
25
+ };
23
26
  }
24
- let name = senderInfo.converseUsername || senderInfo.address;
25
27
 
26
28
  let sendUrl = `${baseUrl}/?transaction_type=send&amount=${amountSend}&token=${tokenSend}&receiver=${senderInfo.address}`;
27
29
  context.send(`${sendUrl}`);
28
- break;
30
+ return {
31
+ code: 200,
32
+ message: `${sendUrl}`,
33
+ };
29
34
  case "swap":
30
35
  // Destructure and validate parameters for the swap command
31
36
  const { amount, token_from, token_to } = params; // [!code hl] // [!code focus]
@@ -34,18 +39,32 @@ export async function handler(context: HandlerContext) {
34
39
  context.reply(
35
40
  "Missing required parameters. Please provide amount, token_from, and token_to.",
36
41
  );
37
- return;
42
+ return {
43
+ code: 400,
44
+ message:
45
+ "Missing required parameters. Please provide amount, token_from, and token_to.",
46
+ };
38
47
  }
39
48
 
40
49
  let swapUrl = `${baseUrl}/?transaction_type=swap&token_from=${token_from}&token_to=${token_to}&amount=${amount}`;
41
50
  context.send(`${swapUrl}`);
42
- break;
51
+ return {
52
+ code: 200,
53
+ message: `${swapUrl}`,
54
+ };
43
55
  case "show": // [!code hl] // [!code focus]
44
56
  // Show the base URL without the transaction path
45
57
  context.reply(`${baseUrl.replace("/transaction", "")}`);
46
- break;
58
+ return {
59
+ code: 200,
60
+ message: `${baseUrl.replace("/transaction", "")}`,
61
+ };
47
62
  default:
48
63
  // Handle unknown commands
49
64
  context.reply("Unknown command. Use help to see all available commands.");
65
+ return {
66
+ code: 400,
67
+ message: "Unknown command. Use help to see all available commands.",
68
+ };
50
69
  }
51
70
  }
@@ -1,57 +1,12 @@
1
1
  import { run, HandlerContext } from "@xmtp/message-kit";
2
- import { handler as splitpayment } from "./handler/splitpayment.js";
3
2
 
4
3
  // Main function to run the app
5
- run(
6
- async (context: HandlerContext) => {
7
- const {
8
- message: { typeId },
9
- } = context;
10
- switch (typeId) {
11
- case "reply":
12
- handleReply(context);
13
- break;
14
- case "remoteStaticAttachment":
15
- handleAttachment(context);
16
- break;
17
- }
18
- if (!context.group) {
19
- context.send("This is a group bot, add this address to a group");
20
- }
21
- },
22
- { attachments: true },
23
- );
24
- async function handleReply(context: HandlerContext) {
25
- const {
26
- v2client,
27
- getReplyChain,
28
- version,
29
- message: {
30
- content: { reference },
31
- },
32
- } = context;
33
-
34
- const { chain, isSenderInChain } = await getReplyChain(
35
- reference,
36
- version,
37
- v2client.address,
38
- );
39
- //await context.skill(chain);
40
- }
41
-
42
- // Handle attachment messages
43
- async function handleAttachment(context: HandlerContext) {
44
- await splitpayment(context);
45
- }
46
-
47
- export async function helpHandler(context: HandlerContext) {
48
- const { skills } = context;
49
- const intro =
50
- "Available experiences:\n" +
51
- skills
52
- ?.flatMap((app) => app.skills)
53
- .map((skill) => `${skill.command} - ${skill.description}`)
54
- .join("\n") +
55
- "\nUse these commands to interact with specific apps.";
56
- context.send(intro);
57
- }
4
+ run(async (context: HandlerContext) => {
5
+ const { group } = context;
6
+
7
+ if (!group) {
8
+ context.send(
9
+ "This This bot only works in group chats. Please add this bot to a group to continue",
10
+ );
11
+ }
12
+ });
@@ -0,0 +1,161 @@
1
+ import "dotenv/config";
2
+ import type { SkillGroup } from "@xmtp/message-kit";
3
+ import OpenAI from "openai";
4
+ const openai = new OpenAI({
5
+ apiKey: process.env.OPEN_AI_API_KEY,
6
+ });
7
+
8
+ type ChatHistoryEntry = { role: string; content: string };
9
+ type ChatHistories = Record<string, ChatHistoryEntry[]>;
10
+ // New ChatMemory class
11
+ class ChatMemory {
12
+ private histories: ChatHistories = {};
13
+
14
+ getHistory(address: string): ChatHistoryEntry[] {
15
+ return this.histories[address] || [];
16
+ }
17
+
18
+ addEntry(address: string, entry: ChatHistoryEntry) {
19
+ if (!this.histories[address]) {
20
+ this.histories[address] = [];
21
+ }
22
+ this.histories[address].push(entry);
23
+ }
24
+
25
+ initializeWithSystem(address: string, systemPrompt: string) {
26
+ if (this.getHistory(address).length === 0) {
27
+ this.addEntry(address, {
28
+ role: "system",
29
+ content: systemPrompt,
30
+ });
31
+ }
32
+ }
33
+
34
+ clear() {
35
+ this.histories = {};
36
+ }
37
+ }
38
+
39
+ // Create singleton instance
40
+ export const chatMemory = new ChatMemory();
41
+
42
+ export const clearMemory = () => {
43
+ chatMemory.clear();
44
+ };
45
+
46
+ export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
47
+ - You can respond with multiple messages if needed. Each message should be separated by a newline character.
48
+ - You can trigger skills by only sending the command in a newline message.
49
+ - Never announce actions without using a command separated by a newline character.
50
+ - Dont answer in markdown format, just answer in plaintext.
51
+ - Do not make guesses or assumptions
52
+ - Only answer if the verified information is in the prompt.
53
+ - Check that you are not missing a command
54
+ - Focus only on helping users with operations detailed below.
55
+ `;
56
+
57
+ export function PROMPT_SKILLS_AND_EXAMPLES(skills: SkillGroup[], tag: string) {
58
+ let foundSkills = skills.filter(
59
+ (skill) => skill.tag == `@${tag.toLowerCase()}`,
60
+ );
61
+ if (!foundSkills.length || !foundSkills[0] || !foundSkills[0].skills)
62
+ return "";
63
+ let returnPrompt = `\nCommands:\n${foundSkills[0].skills
64
+ .map((skill) => skill.command)
65
+ .join("\n")}\n\nExamples:\n${foundSkills[0].skills
66
+ .map((skill) => skill.examples)
67
+ .join("\n")}`;
68
+ return returnPrompt;
69
+ }
70
+
71
+ export async function textGeneration(
72
+ memoryKey: string,
73
+ userPrompt: string,
74
+ systemPrompt: string,
75
+ ) {
76
+ if (!memoryKey) {
77
+ clearMemory();
78
+ }
79
+ let messages = chatMemory.getHistory(memoryKey);
80
+ chatMemory.initializeWithSystem(memoryKey, systemPrompt);
81
+ if (messages.length === 0) {
82
+ messages.push({
83
+ role: "system",
84
+ content: systemPrompt,
85
+ });
86
+ }
87
+ messages.push({
88
+ role: "user",
89
+ content: userPrompt,
90
+ });
91
+ try {
92
+ const response = await openai.chat.completions.create({
93
+ model: "gpt-4o",
94
+ messages: messages as any,
95
+ });
96
+ const reply = response.choices[0].message.content;
97
+ messages.push({
98
+ role: "assistant",
99
+ content: reply || "No response from OpenAI.",
100
+ });
101
+ const cleanedReply = parseMarkdown(reply as string);
102
+ chatMemory.addEntry(memoryKey, {
103
+ role: "assistant",
104
+ content: cleanedReply,
105
+ });
106
+ return { reply: cleanedReply, history: messages };
107
+ } catch (error) {
108
+ console.error("Failed to fetch from OpenAI:", error);
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ export async function processMultilineResponse(
114
+ memoryKey: string,
115
+ reply: string,
116
+ context: any,
117
+ ) {
118
+ if (!memoryKey) {
119
+ clearMemory();
120
+ }
121
+ let messages = reply
122
+ .split("\n")
123
+ .map((message: string) => parseMarkdown(message))
124
+ .filter((message): message is string => message.length > 0);
125
+
126
+ console.log(messages);
127
+ for (const message of messages) {
128
+ if (message.startsWith("/")) {
129
+ const response = await context.executeSkill(message);
130
+ if (response && typeof response.message === "string") {
131
+ let msg = parseMarkdown(response.message);
132
+ chatMemory.addEntry(memoryKey, {
133
+ role: "system",
134
+ content: msg,
135
+ });
136
+ await context.send(response.message);
137
+ }
138
+ } else {
139
+ await context.send(message);
140
+ }
141
+ }
142
+ }
143
+ export function parseMarkdown(message: string) {
144
+ let trimmedMessage = message;
145
+ // Remove bold and underline markdown
146
+ trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
147
+ // Remove markdown links, keeping only the URL
148
+ trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
149
+ // Remove markdown headers
150
+ trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
151
+ // Remove inline code formatting
152
+ trimmedMessage = trimmedMessage?.replace(/`([^`]+)`/g, "$1");
153
+ // Remove single backticks at the start or end of the message
154
+ trimmedMessage = trimmedMessage?.replace(/^`|`$/g, "");
155
+ // Remove leading and trailing whitespace
156
+ trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
157
+ // Remove any remaining leading or trailing whitespace
158
+ trimmedMessage = trimmedMessage.trim();
159
+
160
+ return trimmedMessage;
161
+ }
@@ -48,7 +48,7 @@ export const chatMemory = new ChatMemory();
48
48
  let chatHistories: ChatHistories = {};
49
49
  export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
50
50
  - You can respond with multiple messages if needed. Each message should be separated by a newline character.
51
- - You can trigger commands by only sending the command in a newline message.
51
+ - You can trigger skills by only sending the command in a newline message.
52
52
  - Never announce actions without using a command separated by a newline character.
53
53
  - Dont answer in markdown format, just answer in plaintext.
54
54
  - Do not make guesses or assumptions
@@ -81,7 +81,7 @@ export async function agentResponse(
81
81
  userPrompt,
82
82
  systemPrompt,
83
83
  );
84
- await processResponseWithSkill(sender.address, reply, context);
84
+ await processMultilineResponse(sender.address, reply, context);
85
85
  } catch (error) {
86
86
  console.error("Error during OpenAI call:", error);
87
87
  await context.reply("An error occurred while processing your request.");
@@ -126,7 +126,7 @@ export async function textGeneration(
126
126
  }
127
127
  }
128
128
 
129
- export async function processResponseWithSkill(
129
+ export async function processMultilineResponse(
130
130
  address: string,
131
131
  reply: string,
132
132
  context: any,
@@ -1,5 +1,6 @@
1
- import { Client } from "@xmtp/xmtp-js";
1
+ import type { Client } from "@xmtp/xmtp-js";
2
2
  import { isAddress } from "viem";
3
+ import type { HandlerContext } from "@xmtp/message-kit";
3
4
 
4
5
  export const converseEndpointURL =
5
6
  "https://converse-website-git-endpoit-ephemerahq.vercel.app";
@@ -16,8 +17,8 @@ export type ConverseProfile = {
16
17
  export type UserInfo = {
17
18
  ensDomain?: string | undefined;
18
19
  address?: string | undefined;
20
+ preferredName: string | undefined;
19
21
  converseUsername?: string | undefined;
20
- preferredName?: string | undefined;
21
22
  ensInfo?: EnsData | undefined;
22
23
  avatar?: string | undefined;
23
24
  };
@@ -48,12 +49,14 @@ export const clearInfoCache = () => {
48
49
  export const getUserInfo = async (
49
50
  key: string,
50
51
  clientAddress?: string,
52
+ context?: HandlerContext,
51
53
  ): Promise<UserInfo | null> => {
52
54
  let data: UserInfo = infoCache.get(key) || {
53
55
  ensDomain: undefined,
54
56
  address: undefined,
55
57
  converseUsername: undefined,
56
58
  ensInfo: undefined,
59
+ preferredName: undefined,
57
60
  };
58
61
  if (isAddress(clientAddress || "")) {
59
62
  data.address = clientAddress;
@@ -74,12 +77,15 @@ export const getUserInfo = async (
74
77
  } else {
75
78
  data.converseUsername = key;
76
79
  }
77
-
80
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
78
81
  let keyToUse = data.address || data.ensDomain || data.converseUsername;
79
82
  let cacheData = keyToUse && infoCache.get(keyToUse);
80
- console.log("Getting user info", { cacheData, keyToUse, data });
83
+ //console.log("Getting user info", { cacheData, keyToUse, data });
81
84
  if (cacheData) return cacheData;
82
85
 
86
+ context?.send(
87
+ "Hey there! Give me a sec while I fetch info about you first...",
88
+ );
83
89
  if (keyToUse?.includes(".eth")) {
84
90
  const response = await fetch(`https://ensdata.net/${keyToUse}`);
85
91
  const ensData: EnsData = (await response.json()) as EnsData;
@@ -102,13 +108,15 @@ export const getUserInfo = async (
102
108
  }),
103
109
  });
104
110
  const converseData = (await response.json()) as ConverseProfile;
105
- if (process.env.MSG_LOG)
106
- console.log("Converse data", keyToUse, converseData);
111
+ /// if (process.env.MSG_LOG === "true")
112
+ //console.log("Converse data", keyToUse, converseData);
107
113
  data.converseUsername =
108
114
  converseData?.formattedName || converseData?.name || undefined;
109
115
  data.address = converseData?.address || undefined;
110
116
  data.avatar = converseData?.avatar || undefined;
111
117
  }
118
+
119
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
112
120
  if (data.address) infoCache.set(data.address, data);
113
121
  return data;
114
122
  };
@@ -123,7 +131,8 @@ export const isOnXMTP = async (
123
131
 
124
132
  export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
125
133
  let { address, ensDomain, converseUsername, preferredName } = userInfo;
126
- let prompt = `User context:
134
+ let prompt = `
135
+ User context:
127
136
  - Start by fetch their domain from or Convese username
128
137
  - Call the user by their name or domain, in case they have one
129
138
  - Ask for a name (if they don't have one) so you can suggest domains.
@@ -132,5 +141,11 @@ export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
132
141
  if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
133
142
  if (converseUsername)
134
143
  prompt += `\n- Converse username is: ${converseUsername}`;
144
+
145
+ prompt = prompt.replace("{ADDRESS}", address || "");
146
+ prompt = prompt.replace("{ENS_DOMAIN}", ensDomain || "");
147
+ prompt = prompt.replace("{CONVERSE_USERNAME}", converseUsername || "");
148
+ prompt = prompt.replace("{PREFERRED_NAME}", preferredName || "");
149
+
135
150
  return prompt;
136
151
  };
@@ -1,5 +1,4 @@
1
- import dotenv from "dotenv";
2
- dotenv.config();
1
+ import "dotenv/config";
3
2
 
4
3
  import OpenAI from "openai";
5
4
  const openai = new OpenAI({
@@ -3,7 +3,7 @@ import { handler as agent } from "./handler/agent.js";
3
3
  import { handler as transaction } from "./handler/transaction.js";
4
4
  import { handler as games } from "./handler/game.js";
5
5
  import { handler as loyalty } from "./handler/loyalty.js";
6
- import { helpHandler } from "./index.js";
6
+ import { handler as groupHelp } from "./handler/helpers.js";
7
7
  import type { SkillGroup } from "@xmtp/message-kit";
8
8
 
9
9
  export const skills: SkillGroup[] = [
@@ -31,6 +31,7 @@ export const skills: SkillGroup[] = [
31
31
  },
32
32
  ],
33
33
  },
34
+
34
35
  {
35
36
  name: "Transactions",
36
37
  description: "Multipurpose transaction frame built onbase.",
@@ -159,10 +160,18 @@ export const skills: SkillGroup[] = [
159
160
  command: "/help",
160
161
  triggers: ["/help"],
161
162
  examples: ["/help"],
162
- handler: helpHandler,
163
+ handler: groupHelp,
163
164
  description: "Get help with the bot.",
164
165
  params: {},
165
166
  },
167
+ {
168
+ command: "/id",
169
+ adminOnly: true,
170
+ handler: groupHelp,
171
+ triggers: ["/id"],
172
+ description: "Get the group ID.",
173
+ params: {},
174
+ },
166
175
  ],
167
176
  },
168
177
  ];