Skip to content

Commit d1e7576

Browse files
authored
Merge branch 'develop' into agentic-JSDoc
2 parents 4d9b3ed + ed40fed commit d1e7576

File tree

10 files changed

+23111
-17814
lines changed

10 files changed

+23111
-17814
lines changed

agent/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@elizaos/plugin-multiversx": "workspace:*",
5353
"@elizaos/plugin-near": "workspace:*",
5454
"@elizaos/plugin-zksync-era": "workspace:*",
55+
"@elizaos/plugin-twitter": "workspace:*",
5556
"@elizaos/plugin-cronoszkevm": "workspace:*",
5657
"@elizaos/plugin-3d-generation": "workspace:*",
5758
"readline": "1.3.0",

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": "0.0.17",
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+
}
+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import {
2+
Action,
3+
IAgentRuntime,
4+
Memory,
5+
State,
6+
composeContext,
7+
elizaLogger,
8+
ModelClass,
9+
formatMessages,
10+
generateObject,
11+
} from "@elizaos/core";
12+
import { Scraper } from "agent-twitter-client";
13+
import { tweetTemplate } from "../templates";
14+
import { isTweetContent, TweetSchema } from "../types";
15+
16+
async function composeTweet(
17+
runtime: IAgentRuntime,
18+
_message: Memory,
19+
state?: State
20+
): Promise<string> {
21+
try {
22+
const context = composeContext({
23+
state,
24+
template: tweetTemplate,
25+
});
26+
27+
const tweetContentObject = await generateObject({
28+
runtime,
29+
context,
30+
modelClass: ModelClass.SMALL,
31+
schema: TweetSchema,
32+
stop: ["\n"],
33+
});
34+
35+
if (!isTweetContent(tweetContentObject.object)) {
36+
elizaLogger.error(
37+
"Invalid tweet content:",
38+
tweetContentObject.object
39+
);
40+
return;
41+
}
42+
43+
const trimmedContent = tweetContentObject.object.text.trim();
44+
45+
// Skip truncation if TWITTER_PREMIUM is true
46+
if (
47+
process.env.TWITTER_PREMIUM?.toLowerCase() !== "true" &&
48+
trimmedContent.length > 180
49+
) {
50+
elizaLogger.warn(
51+
`Tweet too long (${trimmedContent.length} chars), truncating...`
52+
);
53+
return trimmedContent.substring(0, 177) + "...";
54+
}
55+
56+
return trimmedContent;
57+
} catch (error) {
58+
elizaLogger.error("Error composing tweet:", error);
59+
throw error;
60+
}
61+
}
62+
63+
async function postTweet(content: string): Promise<boolean> {
64+
try {
65+
const scraper = new Scraper();
66+
const username = process.env.TWITTER_USERNAME;
67+
const password = process.env.TWITTER_PASSWORD;
68+
const email = process.env.TWITTER_EMAIL;
69+
const twitter2faSecret = process.env.TWITTER_2FA_SECRET;
70+
71+
if (!username || !password) {
72+
elizaLogger.error(
73+
"Twitter credentials not configured in environment"
74+
);
75+
return false;
76+
}
77+
78+
// Login with credentials
79+
await scraper.login(username, password, email, twitter2faSecret);
80+
if (!(await scraper.isLoggedIn())) {
81+
elizaLogger.error("Failed to login to Twitter");
82+
return false;
83+
}
84+
85+
// Send the tweet
86+
elizaLogger.log("Attempting to send tweet:", content);
87+
const result = await scraper.sendTweet(content);
88+
89+
const body = await result.json();
90+
elizaLogger.log("Tweet response:", body);
91+
92+
// Check for Twitter API errors
93+
if (body.errors) {
94+
const error = body.errors[0];
95+
elizaLogger.error(
96+
`Twitter API error (${error.code}): ${error.message}`
97+
);
98+
return false;
99+
}
100+
101+
// Check for successful tweet creation
102+
if (!body?.data?.create_tweet?.tweet_results?.result) {
103+
elizaLogger.error(
104+
"Failed to post tweet: No tweet result in response"
105+
);
106+
return false;
107+
}
108+
109+
return true;
110+
} catch (error) {
111+
// Log the full error details
112+
elizaLogger.error("Error posting tweet:", {
113+
message: error.message,
114+
stack: error.stack,
115+
name: error.name,
116+
cause: error.cause,
117+
});
118+
return false;
119+
}
120+
}
121+
122+
export const postAction: Action = {
123+
name: "POST_TWEET",
124+
similes: ["TWEET", "POST", "SEND_TWEET"],
125+
description: "Post a tweet to Twitter",
126+
validate: async (
127+
runtime: IAgentRuntime,
128+
message: Memory,
129+
state?: State
130+
) => {
131+
const hasCredentials =
132+
!!process.env.TWITTER_USERNAME && !!process.env.TWITTER_PASSWORD;
133+
elizaLogger.log(`Has credentials: ${hasCredentials}`);
134+
135+
return hasCredentials;
136+
},
137+
handler: async (
138+
runtime: IAgentRuntime,
139+
message: Memory,
140+
state?: State
141+
): Promise<boolean> => {
142+
try {
143+
// Generate tweet content using context
144+
const tweetContent = await composeTweet(runtime, message, state);
145+
146+
if (!tweetContent) {
147+
elizaLogger.error("No content generated for tweet");
148+
return false;
149+
}
150+
151+
elizaLogger.log(`Generated tweet content: ${tweetContent}`);
152+
153+
// Check for dry run mode - explicitly check for string "true"
154+
if (
155+
process.env.TWITTER_DRY_RUN &&
156+
process.env.TWITTER_DRY_RUN.toLowerCase() === "true"
157+
) {
158+
elizaLogger.info(
159+
`Dry run: would have posted tweet: ${tweetContent}`
160+
);
161+
return true;
162+
}
163+
164+
return await postTweet(tweetContent);
165+
} catch (error) {
166+
elizaLogger.error("Error in post action:", error);
167+
return false;
168+
}
169+
},
170+
examples: [
171+
[
172+
{
173+
user: "{{user1}}",
174+
content: { text: "You should tweet that" },
175+
},
176+
{
177+
user: "{{agentName}}",
178+
content: {
179+
text: "I'll share this update with my followers right away!",
180+
action: "POST_TWEET",
181+
},
182+
},
183+
],
184+
[
185+
{
186+
user: "{{user1}}",
187+
content: { text: "Post this tweet" },
188+
},
189+
{
190+
user: "{{agentName}}",
191+
content: {
192+
text: "I'll post that as a tweet now.",
193+
action: "POST_TWEET",
194+
},
195+
},
196+
],
197+
[
198+
{
199+
user: "{{user1}}",
200+
content: { text: "Share that on Twitter" },
201+
},
202+
{
203+
user: "{{agentName}}",
204+
content: {
205+
text: "I'll share this message on Twitter.",
206+
action: "POST_TWEET",
207+
},
208+
},
209+
],
210+
[
211+
{
212+
user: "{{user1}}",
213+
content: { text: "Post that on X" },
214+
},
215+
{
216+
user: "{{agentName}}",
217+
content: {
218+
text: "I'll post this message on X right away.",
219+
action: "POST_TWEET",
220+
},
221+
},
222+
],
223+
[
224+
{
225+
user: "{{user1}}",
226+
content: { text: "You should put that on X dot com" },
227+
},
228+
{
229+
user: "{{agentName}}",
230+
content: {
231+
text: "I'll put this message up on X.com now.",
232+
action: "POST_TWEET",
233+
},
234+
},
235+
],
236+
],
237+
};

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;
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export const tweetTemplate = `
2+
# Context
3+
{{recentMessages}}
4+
5+
# Topics
6+
{{topics}}
7+
8+
# Post Directions
9+
{{postDirections}}
10+
11+
# Recent interactions between {{agentName}} and other users:
12+
{{recentPostInteractions}}
13+
14+
# Task
15+
Generate a tweet that:
16+
1. Relates to the recent conversation or requested topic
17+
2. Matches the character's style and voice
18+
3. Is concise and engaging
19+
4. Must be UNDER 180 characters (this is a strict requirement)
20+
5. Speaks from the perspective of {{agentName}}
21+
22+
Generate only the tweet text, no other commentary.`;

packages/plugin-twitter/src/types.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { z } from "zod";
2+
3+
export interface TweetContent {
4+
text: string;
5+
}
6+
7+
export const TweetSchema = z.object({
8+
text: z.string().describe("The text of the tweet"),
9+
});
10+
11+
export const isTweetContent = (obj: any): obj is TweetContent => {
12+
return TweetSchema.safeParse(obj).success;
13+
};

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)