Skip to content

Commit 24be11b

Browse files
committed
twitter convo action
1 parent 4c658d7 commit 24be11b

File tree

7 files changed

+319
-60
lines changed

7 files changed

+319
-60
lines changed

agent/package.json

+61-60
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,63 @@
11
{
2-
"name": "@elizaos/agent",
3-
"version": "0.1.7-alpha.1",
4-
"main": "src/index.ts",
5-
"type": "module",
6-
"scripts": {
7-
"start": "node --loader ts-node/esm src/index.ts",
8-
"dev": "node --loader ts-node/esm src/index.ts",
9-
"check-types": "tsc --noEmit"
10-
},
11-
"nodemonConfig": {
12-
"watch": [
13-
"src",
14-
"../core/dist"
15-
],
16-
"ext": "ts,json",
17-
"exec": "node --enable-source-maps --loader ts-node/esm src/index.ts"
18-
},
19-
"dependencies": {
20-
"@elizaos/adapter-postgres": "workspace:*",
21-
"@elizaos/adapter-redis": "workspace:*",
22-
"@elizaos/adapter-sqlite": "workspace:*",
23-
"@elizaos/client-auto": "workspace:*",
24-
"@elizaos/client-direct": "workspace:*",
25-
"@elizaos/client-discord": "workspace:*",
26-
"@elizaos/client-farcaster": "workspace:*",
27-
"@elizaos/client-lens": "workspace:*",
28-
"@elizaos/client-telegram": "workspace:*",
29-
"@elizaos/client-twitter": "workspace:*",
30-
"@elizaos/client-slack": "workspace:*",
31-
"@elizaos/core": "workspace:*",
32-
"@elizaos/plugin-0g": "workspace:*",
33-
"@elizaos/plugin-aptos": "workspace:*",
34-
"@elizaos/plugin-bootstrap": "workspace:*",
35-
"@elizaos/plugin-intiface": "workspace:*",
36-
"@elizaos/plugin-coinbase": "workspace:*",
37-
"@elizaos/plugin-conflux": "workspace:*",
38-
"@elizaos/plugin-evm": "workspace:*",
39-
"@elizaos/plugin-flow": "workspace:*",
40-
"@elizaos/plugin-story": "workspace:*",
41-
"@elizaos/plugin-goat": "workspace:*",
42-
"@elizaos/plugin-icp": "workspace:*",
43-
"@elizaos/plugin-image-generation": "workspace:*",
44-
"@elizaos/plugin-nft-generation": "workspace:*",
45-
"@elizaos/plugin-node": "workspace:*",
46-
"@elizaos/plugin-solana": "workspace:*",
47-
"@elizaos/plugin-starknet": "workspace:*",
48-
"@elizaos/plugin-ton": "workspace:*",
49-
"@elizaos/plugin-sui": "workspace:*",
50-
"@elizaos/plugin-tee": "workspace:*",
51-
"@elizaos/plugin-multiversx": "workspace:*",
52-
"@elizaos/plugin-near": "workspace:*",
53-
"@elizaos/plugin-zksync-era": "workspace:*",
54-
"readline": "1.3.0",
55-
"ws": "8.18.0",
56-
"yargs": "17.7.2"
57-
},
58-
"devDependencies": {
59-
"ts-node": "10.9.2",
60-
"tsup": "8.3.5"
61-
}
2+
"name": "@elizaos/agent",
3+
"version": "0.1.7-alpha.1",
4+
"main": "src/index.ts",
5+
"type": "module",
6+
"scripts": {
7+
"start": "node --loader ts-node/esm src/index.ts",
8+
"dev": "node --loader ts-node/esm src/index.ts",
9+
"check-types": "tsc --noEmit"
10+
},
11+
"nodemonConfig": {
12+
"watch": [
13+
"src",
14+
"../core/dist"
15+
],
16+
"ext": "ts,json",
17+
"exec": "node --enable-source-maps --loader ts-node/esm src/index.ts"
18+
},
19+
"dependencies": {
20+
"@elizaos/adapter-postgres": "workspace:*",
21+
"@elizaos/adapter-redis": "workspace:*",
22+
"@elizaos/adapter-sqlite": "workspace:*",
23+
"@elizaos/client-auto": "workspace:*",
24+
"@elizaos/client-direct": "workspace:*",
25+
"@elizaos/client-discord": "workspace:*",
26+
"@elizaos/client-farcaster": "workspace:*",
27+
"@elizaos/client-lens": "workspace:*",
28+
"@elizaos/client-telegram": "workspace:*",
29+
"@elizaos/client-twitter": "workspace:*",
30+
"@elizaos/client-slack": "workspace:*",
31+
"@elizaos/core": "workspace:*",
32+
"@elizaos/plugin-0g": "workspace:*",
33+
"@elizaos/plugin-aptos": "workspace:*",
34+
"@elizaos/plugin-bootstrap": "workspace:*",
35+
"@elizaos/plugin-intiface": "workspace:*",
36+
"@elizaos/plugin-coinbase": "workspace:*",
37+
"@elizaos/plugin-conflux": "workspace:*",
38+
"@elizaos/plugin-evm": "workspace:*",
39+
"@elizaos/plugin-flow": "workspace:*",
40+
"@elizaos/plugin-story": "workspace:*",
41+
"@elizaos/plugin-goat": "workspace:*",
42+
"@elizaos/plugin-icp": "workspace:*",
43+
"@elizaos/plugin-image-generation": "workspace:*",
44+
"@elizaos/plugin-nft-generation": "workspace:*",
45+
"@elizaos/plugin-node": "workspace:*",
46+
"@elizaos/plugin-solana": "workspace:*",
47+
"@elizaos/plugin-starknet": "workspace:*",
48+
"@elizaos/plugin-ton": "workspace:*",
49+
"@elizaos/plugin-sui": "workspace:*",
50+
"@elizaos/plugin-tee": "workspace:*",
51+
"@elizaos/plugin-multiversx": "workspace:*",
52+
"@elizaos/plugin-near": "workspace:*",
53+
"@elizaos/plugin-zksync-era": "workspace:*",
54+
"@elizaos/plugin-twitter": "workspace:*",
55+
"readline": "1.3.0",
56+
"ws": "8.18.0",
57+
"yargs": "17.7.2"
58+
},
59+
"devDependencies": {
60+
"ts-node": "10.9.2",
61+
"tsup": "8.3.5"
62+
}
6263
}

packages/plugin-twitter/.npmignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*
2+
3+
!dist/**
4+
!package.json
5+
!readme.md
6+
!tsup.config.ts

packages/plugin-twitter/package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@elizaos/plugin-twitter",
3+
"version": "0.1.7-alpha.1",
4+
"main": "dist/index.js",
5+
"type": "module",
6+
"types": "dist/index.d.ts",
7+
"dependencies": {
8+
"@elizaos/core": "workspace:*",
9+
"agent-twitter-client": "^1.0.0",
10+
"tsup": "8.3.5"
11+
},
12+
"scripts": {
13+
"build": "tsup --format esm --dts",
14+
"dev": "tsup --format esm --dts --watch",
15+
"test": "vitest run"
16+
}
17+
}
+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import {
2+
Action,
3+
IAgentRuntime,
4+
Memory,
5+
State,
6+
composeContext,
7+
elizaLogger,
8+
generateText,
9+
ModelClass,
10+
formatMessages,
11+
} from "@elizaos/core";
12+
import { Scraper } from "agent-twitter-client";
13+
14+
async function composeTweet(
15+
runtime: IAgentRuntime,
16+
message: Memory,
17+
state?: State
18+
): Promise<string> {
19+
try {
20+
// Get recent conversation history
21+
const recentMessages = await runtime.messageManager.getMemories({
22+
roomId: message.roomId,
23+
count: 5,
24+
});
25+
26+
const formattedHistory = formatMessages({
27+
messages: recentMessages,
28+
actors: state?.actorsData,
29+
});
30+
31+
// Template for generating the tweet
32+
const tweetTemplate = `
33+
# Context
34+
Recent conversation:
35+
${formattedHistory}
36+
37+
Character style:
38+
${runtime.character.style.post.join("\n")}
39+
40+
Topics of expertise:
41+
${runtime.character.topics.join(", ")}
42+
43+
# Task
44+
Generate a tweet that:
45+
1. Relates to the recent conversation or requested topic
46+
2. Matches the character's style and voice
47+
3. Is concise and engaging
48+
4. Must be UNDER 180 characters (this is a strict requirement)
49+
5. Speaks from the perspective of ${runtime.character.name}
50+
51+
Generate only the tweet text, no other commentary.`;
52+
53+
const context = await composeContext({
54+
state,
55+
template: tweetTemplate,
56+
});
57+
58+
const tweetContent = await generateText({
59+
runtime,
60+
context,
61+
modelClass: ModelClass.SMALL,
62+
stop: ["\n"],
63+
});
64+
65+
const trimmedContent = tweetContent.trim();
66+
67+
// Enforce character limit
68+
if (trimmedContent.length > 180) {
69+
elizaLogger.warn(
70+
`Tweet too long (${trimmedContent.length} chars), truncating...`
71+
);
72+
return trimmedContent.substring(0, 177) + "...";
73+
}
74+
75+
return trimmedContent;
76+
} catch (error) {
77+
elizaLogger.error("Error composing tweet:", error);
78+
throw error;
79+
}
80+
}
81+
82+
async function postTweet(content: string): Promise<boolean> {
83+
try {
84+
const scraper = new Scraper();
85+
const username = process.env.TWITTER_USERNAME;
86+
const password = process.env.TWITTER_PASSWORD;
87+
const email = process.env.TWITTER_EMAIL;
88+
const twitter2faSecret = process.env.TWITTER_2FA_SECRET;
89+
90+
if (!username || !password) {
91+
throw new Error(
92+
"Twitter credentials not configured in environment"
93+
);
94+
}
95+
96+
// Login with credentials
97+
await scraper.login(username, password, email, twitter2faSecret);
98+
if (!(await scraper.isLoggedIn())) {
99+
throw new Error("Failed to login to Twitter");
100+
}
101+
102+
// Send the tweet
103+
elizaLogger.log("Attempting to send tweet:", content);
104+
const result = await scraper.sendTweet(content);
105+
106+
const body = await result.json();
107+
elizaLogger.log("Tweet response:", body);
108+
109+
// Check for Twitter API errors
110+
if (body.errors) {
111+
const error = body.errors[0];
112+
throw new Error(
113+
`Twitter API error (${error.code}): ${error.message}`
114+
);
115+
}
116+
117+
// Check for successful tweet creation
118+
if (!body?.data?.create_tweet?.tweet_results?.result) {
119+
throw new Error(
120+
"Failed to post tweet: No tweet result in response"
121+
);
122+
}
123+
124+
return true;
125+
} catch (error) {
126+
// Log the full error details
127+
elizaLogger.error("Error posting tweet:", {
128+
message: error.message,
129+
stack: error.stack,
130+
name: error.name,
131+
cause: error.cause,
132+
});
133+
return false;
134+
}
135+
}
136+
137+
export const postAction: Action = {
138+
name: "POST_TWEET",
139+
similes: ["TWEET", "POST", "SEND_TWEET"],
140+
description: "Post a tweet to Twitter",
141+
validate: async (
142+
runtime: IAgentRuntime,
143+
message: Memory,
144+
state?: State
145+
) => {
146+
const hasCredentials =
147+
!!process.env.TWITTER_USERNAME && !!process.env.TWITTER_PASSWORD;
148+
elizaLogger.log(`Has credentials: ${hasCredentials}`);
149+
150+
return hasCredentials;
151+
},
152+
handler: async (
153+
runtime: IAgentRuntime,
154+
message: Memory,
155+
state?: State
156+
): Promise<boolean> => {
157+
try {
158+
// Generate tweet content using context
159+
const tweetContent = await composeTweet(runtime, message, state);
160+
161+
if (!tweetContent) {
162+
elizaLogger.error("No content generated for tweet");
163+
return false;
164+
}
165+
166+
elizaLogger.log(`Generated tweet content: ${tweetContent}`);
167+
168+
// Check for dry run mode - explicitly check for string "true"
169+
if (
170+
process.env.TWITTER_DRY_RUN &&
171+
process.env.TWITTER_DRY_RUN.toLowerCase() === "true"
172+
) {
173+
elizaLogger.info(
174+
`Dry run: would have posted tweet: ${tweetContent}`
175+
);
176+
return true;
177+
}
178+
179+
return await postTweet(tweetContent);
180+
} catch (error) {
181+
elizaLogger.error("Error in post action:", error);
182+
return false;
183+
}
184+
},
185+
examples: [
186+
[
187+
{
188+
user: "{{user1}}",
189+
content: { text: "Share your thoughts on AI" },
190+
},
191+
{
192+
user: "{{agentName}}",
193+
content: {
194+
text: "The future of AI lies in responsible development and ethical considerations. We must ensure it benefits all of humanity.",
195+
action: "POST_TWEET",
196+
},
197+
},
198+
],
199+
],
200+
};

packages/plugin-twitter/src/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Plugin } from "@elizaos/core";
2+
import { postAction } from "./actions/post";
3+
4+
export const twitterPlugin: Plugin = {
5+
name: "twitter",
6+
description: "Twitter integration plugin for posting tweets",
7+
actions: [postAction],
8+
evaluators: [],
9+
providers: [],
10+
};
11+
12+
export default twitterPlugin;

packages/plugin-twitter/tsconfig.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "../core/tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": "src",
6+
"types": [
7+
"node"
8+
]
9+
},
10+
"include": [
11+
"src/**/*.ts"
12+
]
13+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from "tsup";
2+
3+
export default defineConfig({
4+
entry: ["src/index.ts"],
5+
outDir: "dist",
6+
sourcemap: true,
7+
clean: true,
8+
format: ["esm"],
9+
external: ["dotenv", "fs", "path", "https", "http", "agentkeepalive"],
10+
});

0 commit comments

Comments
 (0)