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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  ];