Skip to content

Commit 510051b

Browse files
authoredJan 18, 2025··
Merge branch 'elizaOS:develop' into develop
2 parents 8ae6ae4 + 3512475 commit 510051b

File tree

15 files changed

+995
-133
lines changed

15 files changed

+995
-133
lines changed
 

‎agent/src/index.ts

+82-65
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,26 @@ export async function loadCharacterFromOnchain(): Promise<Character[]> {
253253
}
254254
}
255255

256-
async function loadCharacterFromUrl(url: string): Promise<Character> {
257-
const response = await fetch(url);
258-
const character = await response.json();
259-
return jsonToCharacter(url, character);
256+
257+
async function loadCharactersFromUrl(url: string): Promise<Character[]> {
258+
try {
259+
const response = await fetch(url);
260+
const responseJson = await response.json();
261+
262+
let characters: Character[] = [];
263+
if (Array.isArray(responseJson)) {
264+
characters = await Promise.all(
265+
responseJson.map((character) => jsonToCharacter(url, character))
266+
);
267+
} else {
268+
const character = await jsonToCharacter(url, responseJson);
269+
characters.push(character);
270+
}
271+
return characters;
272+
} catch (e) {
273+
elizaLogger.error(`Error loading character(s) from ${url}: ${e}`);
274+
process.exit(1);
275+
}
260276
}
261277

262278
async function jsonToCharacter(
@@ -309,6 +325,61 @@ async function loadCharacter(filePath: string): Promise<Character> {
309325
return jsonToCharacter(filePath, character);
310326
}
311327

328+
async function loadCharacterTryPath(characterPath: string): Promise<Character> {
329+
let content: string | null = null;
330+
let resolvedPath = "";
331+
332+
// Try different path resolutions in order
333+
const pathsToTry = [
334+
characterPath, // exact path as specified
335+
path.resolve(process.cwd(), characterPath), // relative to cwd
336+
path.resolve(process.cwd(), "agent", characterPath), // Add this
337+
path.resolve(__dirname, characterPath), // relative to current script
338+
path.resolve(__dirname, "characters", path.basename(characterPath)), // relative to agent/characters
339+
path.resolve(__dirname, "../characters", path.basename(characterPath)), // relative to characters dir from agent
340+
path.resolve(
341+
__dirname,
342+
"../../characters",
343+
path.basename(characterPath)
344+
), // relative to project root characters dir
345+
];
346+
347+
elizaLogger.info(
348+
"Trying paths:",
349+
pathsToTry.map((p) => ({
350+
path: p,
351+
exists: fs.existsSync(p),
352+
}))
353+
);
354+
355+
for (const tryPath of pathsToTry) {
356+
content = tryLoadFile(tryPath);
357+
if (content !== null) {
358+
resolvedPath = tryPath;
359+
break;
360+
}
361+
}
362+
363+
if (content === null) {
364+
elizaLogger.error(
365+
`Error loading character from ${characterPath}: File not found in any of the expected locations`
366+
);
367+
elizaLogger.error("Tried the following paths:");
368+
pathsToTry.forEach((p) => elizaLogger.error(` - ${p}`));
369+
throw new Error(
370+
`Error loading character from ${characterPath}: File not found in any of the expected locations`
371+
);
372+
}
373+
try {
374+
const character: Character = await loadCharacter(resolvedPath);
375+
elizaLogger.info(`Successfully loaded character from: ${resolvedPath}`);
376+
return character;
377+
} catch (e) {
378+
elizaLogger.error(`Error parsing character from ${resolvedPath}: ${e}`);
379+
throw new Error(`Error parsing character from ${resolvedPath}: ${e}`);
380+
}
381+
}
382+
312383
function commaSeparatedStringToArray(commaSeparated: string): string[] {
313384
return commaSeparated?.split(",").map((value) => value.trim());
314385
}
@@ -321,68 +392,11 @@ export async function loadCharacters(
321392

322393
if (characterPaths?.length > 0) {
323394
for (const characterPath of characterPaths) {
324-
let content: string | null = null;
325-
let resolvedPath = "";
326-
327-
// Try different path resolutions in order
328-
const pathsToTry = [
329-
characterPath, // exact path as specified
330-
path.resolve(process.cwd(), characterPath), // relative to cwd
331-
path.resolve(process.cwd(), "agent", characterPath), // Add this
332-
path.resolve(__dirname, characterPath), // relative to current script
333-
path.resolve(
334-
__dirname,
335-
"characters",
336-
path.basename(characterPath)
337-
), // relative to agent/characters
338-
path.resolve(
339-
__dirname,
340-
"../characters",
341-
path.basename(characterPath)
342-
), // relative to characters dir from agent
343-
path.resolve(
344-
__dirname,
345-
"../../characters",
346-
path.basename(characterPath)
347-
), // relative to project root characters dir
348-
];
349-
350-
elizaLogger.info(
351-
"Trying paths:",
352-
pathsToTry.map((p) => ({
353-
path: p,
354-
exists: fs.existsSync(p),
355-
}))
356-
);
357-
358-
for (const tryPath of pathsToTry) {
359-
content = tryLoadFile(tryPath);
360-
if (content !== null) {
361-
resolvedPath = tryPath;
362-
break;
363-
}
364-
}
365-
366-
if (content === null) {
367-
elizaLogger.error(
368-
`Error loading character from ${characterPath}: File not found in any of the expected locations`
369-
);
370-
elizaLogger.error("Tried the following paths:");
371-
pathsToTry.forEach((p) => elizaLogger.error(` - ${p}`));
372-
process.exit(1);
373-
}
374-
375395
try {
376-
const character: Character = await loadCharacter(resolvedPath);
377-
396+
const character: Character =
397+
await loadCharacterTryPath(characterPath);
378398
loadedCharacters.push(character);
379-
elizaLogger.info(
380-
`Successfully loaded character from: ${resolvedPath}`
381-
);
382399
} catch (e) {
383-
elizaLogger.error(
384-
`Error parsing character from ${resolvedPath}: ${e}`
385-
);
386400
process.exit(1);
387401
}
388402
}
@@ -394,8 +408,8 @@ export async function loadCharacters(
394408
process.env.REMOTE_CHARACTER_URLS
395409
);
396410
for (const characterUrl of characterUrls) {
397-
const character = await loadCharacterFromUrl(characterUrl);
398-
loadedCharacters.push(character);
411+
const characters = await loadCharactersFromUrl(characterUrl);
412+
loadedCharacters.push(...characters);
399413
}
400414
}
401415

@@ -1260,6 +1274,9 @@ const startAgents = async () => {
12601274
return startAgent(character, directClient);
12611275
};
12621276

1277+
directClient.loadCharacterTryPath = loadCharacterTryPath;
1278+
directClient.jsonToCharacter = jsonToCharacter;
1279+
12631280
directClient.start(serverPort);
12641281

12651282
if (serverPort !== Number.parseInt(settings.SERVER_PORT || "3000")) {

‎client/src/components/ui/chat/chat-bubble.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,7 @@ const ChatBubbleMessage = React.forwardRef<
125125
ChatBubbleMessage.displayName = "ChatBubbleMessage";
126126

127127
// ChatBubbleTimestamp
128-
interface ChatBubbleTimestampProps
129-
extends React.HTMLAttributes<HTMLDivElement> {
128+
interface ChatBubbleTimestampProps extends React.HTMLAttributes<HTMLDivElement> {
130129
timestamp: string;
131130
}
132131

‎client/version.sh

+9-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44
LERNA_FILE="../lerna.json"
55

66
# Check if lerna.json exists
7-
if [ ! -f "$LERNA_FILE" ]; then
8-
echo "Error: $LERNA_FILE does not exist."
7+
if [ ! -f "${LERNA_FILE}" ]; then
8+
echo "Error: ${LERNA_FILE} does not exist."
9+
exit 1
10+
fi
11+
12+
# Check if we have write permissions to the destination directory
13+
if [ ! -w "src/lib" ]; then
14+
echo "Error: No write permission to src/lib directory."
915
exit 1
1016
fi
1117

@@ -22,4 +28,4 @@ fi
2228
echo "{\"version\": \"$VERSION\"}" > src/lib/info.json
2329

2430
# Confirm success
25-
echo "info.json created with version: $VERSION"
31+
echo "info.json created with version: $VERSION"

‎packages/client-direct/src/api.ts

+76-21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type UUID,
1010
validateCharacterConfig,
1111
ServiceType,
12+
Character,
1213
} from "@elizaos/core";
1314

1415
import type { TeeLogQuery, TeeLogService } from "@elizaos/plugin-tee-log";
@@ -114,9 +115,8 @@ export function createApiRouter(
114115
if (agent) {
115116
agent.stop();
116117
directClient.unregisterAgent(agent);
117-
res.status(204).send();
118-
}
119-
else {
118+
res.status(204).json({ success: true });
119+
} else {
120120
res.status(404).json({ error: "Agent not found" });
121121
}
122122
});
@@ -269,18 +269,20 @@ export function createApiRouter(
269269

270270
for (const agentRuntime of agents.values()) {
271271
const teeLogService = agentRuntime
272-
.getService<TeeLogService>(
273-
ServiceType.TEE_LOG
274-
)
275-
.getInstance();
272+
.getService<TeeLogService>(ServiceType.TEE_LOG)
273+
.getInstance();
276274

277275
const agents = await teeLogService.getAllAgents();
278-
allAgents.push(...agents)
276+
allAgents.push(...agents);
279277
}
280278

281279
const runtime: AgentRuntime = agents.values().next().value;
282-
const teeLogService = runtime.getService<TeeLogService>(ServiceType.TEE_LOG).getInstance();
283-
const attestation = await teeLogService.generateAttestation(JSON.stringify(allAgents));
280+
const teeLogService = runtime
281+
.getService<TeeLogService>(ServiceType.TEE_LOG)
282+
.getInstance();
283+
const attestation = await teeLogService.generateAttestation(
284+
JSON.stringify(allAgents)
285+
);
284286
res.json({ agents: allAgents, attestation: attestation });
285287
} catch (error) {
286288
elizaLogger.error("Failed to get TEE agents:", error);
@@ -300,13 +302,13 @@ export function createApiRouter(
300302
}
301303

302304
const teeLogService = agentRuntime
303-
.getService<TeeLogService>(
304-
ServiceType.TEE_LOG
305-
)
306-
.getInstance();
305+
.getService<TeeLogService>(ServiceType.TEE_LOG)
306+
.getInstance();
307307

308308
const teeAgent = await teeLogService.getAgent(agentId);
309-
const attestation = await teeLogService.generateAttestation(JSON.stringify(teeAgent));
309+
const attestation = await teeLogService.generateAttestation(
310+
JSON.stringify(teeAgent)
311+
);
310312
res.json({ agent: teeAgent, attestation: attestation });
311313
} catch (error) {
312314
elizaLogger.error("Failed to get TEE agent:", error);
@@ -335,12 +337,16 @@ export function createApiRouter(
335337
};
336338
const agentRuntime: AgentRuntime = agents.values().next().value;
337339
const teeLogService = agentRuntime
338-
.getService<TeeLogService>(
339-
ServiceType.TEE_LOG
340-
)
340+
.getService<TeeLogService>(ServiceType.TEE_LOG)
341341
.getInstance();
342-
const pageQuery = await teeLogService.getLogs(teeLogQuery, page, pageSize);
343-
const attestation = await teeLogService.generateAttestation(JSON.stringify(pageQuery));
342+
const pageQuery = await teeLogService.getLogs(
343+
teeLogQuery,
344+
page,
345+
pageSize
346+
);
347+
const attestation = await teeLogService.generateAttestation(
348+
JSON.stringify(pageQuery)
349+
);
344350
res.json({
345351
logs: pageQuery,
346352
attestation: attestation,
@@ -354,6 +360,55 @@ export function createApiRouter(
354360
}
355361
);
356362

363+
router.post("/agent/start", async (req, res) => {
364+
const { characterPath, characterJson } = req.body;
365+
console.log("characterPath:", characterPath);
366+
console.log("characterJson:", characterJson);
367+
try {
368+
let character: Character;
369+
if (characterJson) {
370+
character = await directClient.jsonToCharacter(
371+
characterPath,
372+
characterJson
373+
);
374+
} else if (characterPath) {
375+
character =
376+
await directClient.loadCharacterTryPath(characterPath);
377+
} else {
378+
throw new Error("No character path or JSON provided");
379+
}
380+
await directClient.startAgent(character);
381+
elizaLogger.log(`${character.name} started`);
382+
383+
res.json({
384+
id: character.id,
385+
character: character,
386+
});
387+
} catch (e) {
388+
elizaLogger.error(`Error parsing character: ${e}`);
389+
res.status(400).json({
390+
error: e.message,
391+
});
392+
return;
393+
}
394+
});
395+
396+
router.post("/agents/:agentId/stop", async (req, res) => {
397+
const agentId = req.params.agentId;
398+
console.log("agentId", agentId);
399+
const agent: AgentRuntime = agents.get(agentId);
400+
401+
// update character
402+
if (agent) {
403+
// stop agent
404+
agent.stop();
405+
directClient.unregisterAgent(agent);
406+
// if it has a different name, the agentId will change
407+
res.json({ success: true });
408+
} else {
409+
res.status(404).json({ error: "Agent not found" });
410+
}
411+
});
412+
357413
return router;
358414
}
359-

‎packages/client-direct/src/index.ts

+37-32
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export class DirectClient {
113113
private agents: Map<string, AgentRuntime>; // container management
114114
private server: any; // Store server instance
115115
public startAgent: Function; // Store startAgent functor
116+
public loadCharacterTryPath: Function; // Store loadCharacterTryPath functor
117+
public jsonToCharacter: Function; // Store jsonToCharacter functor
116118

117119
constructor() {
118120
elizaLogger.log("DirectClient constructor");
@@ -136,7 +138,6 @@ export class DirectClient {
136138
const apiRouter = createApiRouter(this.agents, this);
137139
this.app.use(apiRouter);
138140

139-
140141
const apiLogRouter = createVerifiableLogApiRouter(this.agents);
141142
this.app.use(apiLogRouter);
142143

@@ -555,38 +556,42 @@ export class DirectClient {
555556
content: contentObj,
556557
};
557558

558-
runtime.messageManager.createMemory(responseMessage).then(() => {
559-
const messageId = stringToUuid(Date.now().toString());
560-
const memory: Memory = {
561-
id: messageId,
562-
agentId: runtime.agentId,
563-
userId,
564-
roomId,
565-
content,
566-
createdAt: Date.now(),
567-
};
568-
569-
// run evaluators (generally can be done in parallel with processActions)
570-
// can an evaluator modify memory? it could but currently doesn't
571-
runtime.evaluate(memory, state).then(() => {
572-
// only need to call if responseMessage.content.action is set
573-
if (contentObj.action) {
574-
// pass memory (query) to any actions to call
575-
runtime.processActions(
576-
memory,
577-
[responseMessage],
578-
state,
579-
async (_newMessages) => {
580-
// FIXME: this is supposed override what the LLM said/decided
581-
// but the promise doesn't make this possible
582-
//message = newMessages;
583-
return [memory];
584-
}
585-
); // 0.674s
586-
}
587-
resolve(true);
559+
runtime.messageManager
560+
.createMemory(responseMessage)
561+
.then(() => {
562+
const messageId = stringToUuid(
563+
Date.now().toString()
564+
);
565+
const memory: Memory = {
566+
id: messageId,
567+
agentId: runtime.agentId,
568+
userId,
569+
roomId,
570+
content,
571+
createdAt: Date.now(),
572+
};
573+
574+
// run evaluators (generally can be done in parallel with processActions)
575+
// can an evaluator modify memory? it could but currently doesn't
576+
runtime.evaluate(memory, state).then(() => {
577+
// only need to call if responseMessage.content.action is set
578+
if (contentObj.action) {
579+
// pass memory (query) to any actions to call
580+
runtime.processActions(
581+
memory,
582+
[responseMessage],
583+
state,
584+
async (_newMessages) => {
585+
// FIXME: this is supposed override what the LLM said/decided
586+
// but the promise doesn't make this possible
587+
//message = newMessages;
588+
return [memory];
589+
}
590+
); // 0.674s
591+
}
592+
resolve(true);
593+
});
588594
});
589-
});
590595
});
591596
res.json({ response: hfOut });
592597
}

‎packages/client-discord/src/constants.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export const TEAM_COORDINATION = {
22
KEYWORDS: [
33
"team",
4-
"everyone",
54
"all agents",
65
"team update",
76
"gm team",

‎packages/client-discord/src/messages.ts

+246
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import type { VoiceManager } from "./voice.ts";
2727
import {
2828
discordShouldRespondTemplate,
2929
discordMessageHandlerTemplate,
30+
discordAutoPostTemplate,
31+
discordAnnouncementHypeTemplate
3032
} from "./templates.ts";
3133
import {
3234
IGNORE_RESPONSE_WORDS,
@@ -48,6 +50,16 @@ interface MessageContext {
4850
timestamp: number;
4951
}
5052

53+
interface AutoPostConfig {
54+
enabled: boolean;
55+
monitorTime: number;
56+
inactivityThreshold: number; // milliseconds
57+
mainChannelId: string;
58+
announcementChannelIds: string[];
59+
lastAutoPost?: number;
60+
minTimeBetweenPosts?: number; // minimum time between auto posts
61+
}
62+
5163
export type InterestChannels = {
5264
[key: string]: {
5365
currentHandler: string | undefined;
@@ -65,16 +77,36 @@ export class MessageManager {
6577
private interestChannels: InterestChannels = {};
6678
private discordClient: any;
6779
private voiceManager: VoiceManager;
80+
//Auto post
81+
private autoPostConfig: AutoPostConfig;
82+
private lastChannelActivity: { [channelId: string]: number } = {};
83+
private autoPostInterval: NodeJS.Timeout;
6884

6985
constructor(discordClient: any, voiceManager: VoiceManager) {
7086
this.client = discordClient.client;
7187
this.voiceManager = voiceManager;
7288
this.discordClient = discordClient;
7389
this.runtime = discordClient.runtime;
7490
this.attachmentManager = new AttachmentManager(this.runtime);
91+
92+
this.autoPostConfig = {
93+
enabled: this.runtime.character.clientConfig?.discord?.autoPost?.enabled || false,
94+
monitorTime: this.runtime.character.clientConfig?.discord?.autoPost?.monitorTime || 300000,
95+
inactivityThreshold: this.runtime.character.clientConfig?.discord?.autoPost?.inactivityThreshold || 3600000, // 1 hour default
96+
mainChannelId: this.runtime.character.clientConfig?.discord?.autoPost?.mainChannelId,
97+
announcementChannelIds: this.runtime.character.clientConfig?.discord?.autoPost?.announcementChannelIds || [],
98+
minTimeBetweenPosts: this.runtime.character.clientConfig?.discord?.autoPost?.minTimeBetweenPosts || 7200000, // 2 hours default
99+
};
100+
101+
if (this.autoPostConfig.enabled) {
102+
this._startAutoPostMonitoring();
103+
}
75104
}
76105

77106
async handleMessage(message: DiscordMessage) {
107+
// Update last activity time for the channel
108+
this.lastChannelActivity[message.channelId] = Date.now();
109+
78110
if (
79111
message.interaction ||
80112
message.author.id ===
@@ -512,6 +544,220 @@ export class MessageManager {
512544
}
513545
}
514546

547+
private _startAutoPostMonitoring(): void {
548+
// Wait for client to be ready
549+
if (!this.client.isReady()) {
550+
elizaLogger.info('[AutoPost Discord] Client not ready, waiting for ready event')
551+
this.client.once('ready', () => {
552+
elizaLogger.info('[AutoPost Discord] Client ready, starting monitoring')
553+
this._initializeAutoPost();
554+
});
555+
} else {
556+
elizaLogger.info('[AutoPost Discord] Client already ready, starting monitoring')
557+
this._initializeAutoPost();
558+
}
559+
}
560+
561+
private _initializeAutoPost(): void {
562+
// Give the client a moment to fully load its cache
563+
setTimeout(() => {
564+
// Monitor with random intervals between 2-6 hours
565+
this.autoPostInterval = setInterval(() => {
566+
this._checkChannelActivity();
567+
}, Math.floor(Math.random() * (4 * 60 * 60 * 1000) + 2 * 60 * 60 * 1000));
568+
569+
// Start monitoring announcement channels
570+
this._monitorAnnouncementChannels();
571+
}, 5000); // 5 second delay to ensure everything is loaded
572+
}
573+
574+
private async _checkChannelActivity(): Promise<void> {
575+
if (!this.autoPostConfig.enabled || !this.autoPostConfig.mainChannelId) return;
576+
577+
const channel = this.client.channels.cache.get(this.autoPostConfig.mainChannelId) as TextChannel;
578+
if (!channel) return;
579+
580+
try {
581+
// Get last message time
582+
const messages = await channel.messages.fetch({ limit: 1 });
583+
const lastMessage = messages.first();
584+
const lastMessageTime = lastMessage ? lastMessage.createdTimestamp : 0;
585+
586+
const now = Date.now();
587+
const timeSinceLastMessage = now - lastMessageTime;
588+
const timeSinceLastAutoPost = now - (this.autoPostConfig.lastAutoPost || 0);
589+
590+
// Add some randomness to the inactivity threshold (±30 minutes)
591+
const randomThreshold = this.autoPostConfig.inactivityThreshold +
592+
(Math.random() * 1800000 - 900000);
593+
594+
// Check if we should post
595+
if ((timeSinceLastMessage > randomThreshold) &&
596+
timeSinceLastAutoPost > (this.autoPostConfig.minTimeBetweenPosts || 0)) {
597+
598+
try {
599+
// Create memory and generate response
600+
const roomId = stringToUuid(channel.id + "-" + this.runtime.agentId);
601+
602+
const memory = {
603+
id: stringToUuid(`autopost-${Date.now()}`),
604+
userId: this.runtime.agentId,
605+
agentId: this.runtime.agentId,
606+
roomId,
607+
content: { text: "AUTO_POST_ENGAGEMENT", source: "discord" },
608+
embedding: getEmbeddingZeroVector(),
609+
createdAt: Date.now()
610+
};
611+
612+
let state = await this.runtime.composeState(memory, {
613+
discordClient: this.client,
614+
discordMessage: null,
615+
agentName: this.runtime.character.name || this.client.user?.displayName
616+
});
617+
618+
// Generate response using template
619+
const context = composeContext({
620+
state,
621+
template: this.runtime.character.templates?.discordAutoPostTemplate || discordAutoPostTemplate
622+
});
623+
624+
const responseContent = await this._generateResponse(memory, state, context);
625+
if (!responseContent?.text) return;
626+
627+
// Send message and update memory
628+
const messages = await sendMessageInChunks(channel, responseContent.text.trim(), null, []);
629+
630+
// Create and store memories
631+
const memories = messages.map(m => ({
632+
id: stringToUuid(m.id + "-" + this.runtime.agentId),
633+
userId: this.runtime.agentId,
634+
agentId: this.runtime.agentId,
635+
content: {
636+
...responseContent,
637+
url: m.url,
638+
},
639+
roomId,
640+
embedding: getEmbeddingZeroVector(),
641+
createdAt: m.createdTimestamp,
642+
}));
643+
644+
for (const m of memories) {
645+
await this.runtime.messageManager.createMemory(m);
646+
}
647+
648+
// Update state and last post time
649+
this.autoPostConfig.lastAutoPost = Date.now();
650+
state = await this.runtime.updateRecentMessageState(state);
651+
await this.runtime.evaluate(memory, state, true);
652+
} catch (error) {
653+
elizaLogger.warn("[AutoPost Discord] Error:", error);
654+
}
655+
} else {
656+
elizaLogger.warn("[AutoPost Discord] Activity within threshold. Not posting.");
657+
}
658+
} catch (error) {
659+
elizaLogger.warn("[AutoPost Discord] Error checking last message:", error);
660+
}
661+
}
662+
663+
private async _monitorAnnouncementChannels(): Promise<void> {
664+
if (!this.autoPostConfig.enabled || !this.autoPostConfig.announcementChannelIds.length) {
665+
elizaLogger.warn('[AutoPost Discord] Auto post config disabled or no announcement channels')
666+
return;
667+
}
668+
669+
for (const announcementChannelId of this.autoPostConfig.announcementChannelIds) {
670+
const channel = this.client.channels.cache.get(announcementChannelId);
671+
672+
if (channel) {
673+
// Check if it's either a text channel or announcement channel
674+
// ChannelType.GuildAnnouncement is 5
675+
// ChannelType.GuildText is 0
676+
if (channel instanceof TextChannel || channel.type === ChannelType.GuildAnnouncement) {
677+
const newsChannel = channel as TextChannel;
678+
try {
679+
newsChannel.createMessageCollector().on('collect', async (message: DiscordMessage) => {
680+
if (message.author.bot || Date.now() - message.createdTimestamp > 300000) return;
681+
682+
const mainChannel = this.client.channels.cache.get(this.autoPostConfig.mainChannelId) as TextChannel;
683+
if (!mainChannel) return;
684+
685+
try {
686+
// Create memory and generate response
687+
const roomId = stringToUuid(mainChannel.id + "-" + this.runtime.agentId);
688+
const memory = {
689+
id: stringToUuid(`announcement-${Date.now()}`),
690+
userId: this.runtime.agentId,
691+
agentId: this.runtime.agentId,
692+
roomId,
693+
content: {
694+
text: message.content,
695+
source: "discord",
696+
metadata: { announcementUrl: message.url }
697+
},
698+
embedding: getEmbeddingZeroVector(),
699+
createdAt: Date.now()
700+
};
701+
702+
let state = await this.runtime.composeState(memory, {
703+
discordClient: this.client,
704+
discordMessage: message,
705+
announcementContent: message?.content,
706+
announcementChannelId: channel.id,
707+
agentName: this.runtime.character.name || this.client.user?.displayName
708+
});
709+
710+
// Generate response using template
711+
const context = composeContext({
712+
state,
713+
template: this.runtime.character.templates?.discordAnnouncementHypeTemplate || discordAnnouncementHypeTemplate
714+
715+
});
716+
717+
const responseContent = await this._generateResponse(memory, state, context);
718+
if (!responseContent?.text) return;
719+
720+
// Send message and update memory
721+
const messages = await sendMessageInChunks(mainChannel, responseContent.text.trim(), null, []);
722+
723+
// Create and store memories
724+
const memories = messages.map(m => ({
725+
id: stringToUuid(m.id + "-" + this.runtime.agentId),
726+
userId: this.runtime.agentId,
727+
agentId: this.runtime.agentId,
728+
content: {
729+
...responseContent,
730+
url: m.url,
731+
},
732+
roomId,
733+
embedding: getEmbeddingZeroVector(),
734+
createdAt: m.createdTimestamp,
735+
}));
736+
737+
for (const m of memories) {
738+
await this.runtime.messageManager.createMemory(m);
739+
}
740+
741+
// Update state
742+
state = await this.runtime.updateRecentMessageState(state);
743+
await this.runtime.evaluate(memory, state, true);
744+
} catch (error) {
745+
elizaLogger.warn("[AutoPost Discord] Announcement Error:", error);
746+
}
747+
});
748+
elizaLogger.info(`[AutoPost Discord] Successfully set up collector for announcement channel: ${newsChannel.name}`);
749+
} catch (error) {
750+
elizaLogger.warn(`[AutoPost Discord] Error setting up announcement channel collector:`, error);
751+
}
752+
} else {
753+
elizaLogger.warn(`[AutoPost Discord] Channel ${announcementChannelId} is not a valid announcement or text channel, type:`, channel.type);
754+
}
755+
} else {
756+
elizaLogger.warn(`[AutoPost Discord] Could not find channel ${announcementChannelId} directly`);
757+
}
758+
}
759+
}
760+
515761
private _isMessageForMe(message: DiscordMessage): boolean {
516762
const isMentioned = message.mentions.users?.has(
517763
this.client.user?.id as string

‎packages/client-discord/src/templates.ts

+72
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,75 @@ Note that {{agentName}} is capable of reading/seeing/hearing various forms of me
121121
122122
# Instructions: Write the next message for {{agentName}}. Include an action, if appropriate. {{actionNames}}
123123
` + messageCompletionFooter;
124+
125+
export const discordAutoPostTemplate =
126+
`# Action Examples
127+
NONE: Respond but perform no additional action. This is the default if the agent is speaking and not doing anything additional.
128+
129+
# Task: Generate an engaging community message as {{agentName}}.
130+
About {{agentName}}:
131+
{{bio}}
132+
{{lore}}
133+
134+
Examples of {{agentName}}'s dialog and actions:
135+
{{characterMessageExamples}}
136+
137+
{{messageDirections}}
138+
139+
# Recent Chat History:
140+
{{recentMessages}}
141+
142+
# Instructions: Write a natural, engaging message to restart community conversation. Focus on:
143+
- Community engagement
144+
- Educational topics
145+
- General discusions
146+
- Support queries
147+
- Keep message warm and inviting
148+
- Maximum 3 lines
149+
- Use 1-2 emojis maximum
150+
- Avoid financial advice
151+
- Stay within known facts
152+
- No team member mentions
153+
- Be hyped, not repetitive
154+
- Be natural, act like a human, connect with the community
155+
- Don't sound so robotic like
156+
- Randomly grab the most recent 5 messages for some context. Validate the context randomly and use that as a reference point for your next message, but not always, only when relevant.
157+
- If the recent messages are mostly from {{agentName}}, make sure to create conversation starters, given there is no messages from others to reference.
158+
- DO NOT REPEAT THE SAME thing that you just said from your recent chat history, start the message different each time, and be organic, non reptitive.
159+
160+
# Instructions: Write the next message for {{agentName}}. Include the "NONE" action only, as the only valid action for auto-posts is "NONE".
161+
` + messageCompletionFooter;
162+
163+
export const discordAnnouncementHypeTemplate =
164+
`# Action Examples
165+
NONE: Respond but perform no additional action. This is the default if the agent is speaking and not doing anything additional.
166+
167+
# Task: Generate announcement hype message as {{agentName}}.
168+
About {{agentName}}:
169+
{{bio}}
170+
{{lore}}
171+
172+
Examples of {{agentName}}'s dialog and actions:
173+
{{characterMessageExamples}}
174+
175+
{{messageDirections}}
176+
177+
# Announcement Content:
178+
{{announcementContent}}
179+
180+
# Instructions: Write an exciting message to bring attention to the announcement. Requirements:
181+
- Reference the announcement channel using <#{{announcementChannelId}}>
182+
- Reference the announcement content to get information about the announcement to use where appropriate to make the message dynamic vs a static post
183+
- Create genuine excitement
184+
- Encourage community participation
185+
- If there are links like Twitter/X posts, encourage users to like/retweet/comment to spread awarenress, but directly say that, wrap that into the post so its natural.
186+
- Stay within announced facts only
187+
- No additional promises or assumptions
188+
- No team member mentions
189+
- Start the message differently each time. Don't start with the same word like "hey", "hey hey", etc. be dynamic
190+
- Address everyone, not as a direct reply to whoever made the announcement or wrote it, but you can reference them
191+
- Maximum 3-7 lines formatted nicely if needed, based on the context of the announcement
192+
- Use 1-2 emojis maximum
193+
194+
# Instructions: Write the next message for {{agentName}}. Include the "NONE" action only, as no other actions are appropriate for announcement hype.
195+
` + messageCompletionFooter;

‎packages/core/src/runtime.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ export class AgentRuntime implements IAgentRuntime {
298298
this.ensureRoomExists(this.agentId);
299299
this.ensureUserExists(
300300
this.agentId,
301-
this.character.name,
301+
this.character.username || this.character.name,
302302
this.character.name
303303
).then(() => {
304304
// postgres needs the user to exist before you can add a participant
@@ -1111,10 +1111,10 @@ export class AgentRuntime implements IAgentRuntime {
11111111
if (!account) {
11121112
await this.databaseAdapter.createAccount({
11131113
id: userId,
1114-
name: name || userName || "Unknown User",
1115-
username: userName || name || "Unknown",
1116-
email: email || (userName || "Bot") + "@" + source || "Unknown", // Temporary
1117-
details: { summary: "" },
1114+
name: name || this.character.name || "Unknown User",
1115+
username: userName || this.character.username || "Unknown",
1116+
email: email || this.character.email || userId, // Temporary
1117+
details: this.character || { summary: "" },
11181118
});
11191119
elizaLogger.success(`User ${userName} created successfully.`);
11201120
}
@@ -1147,7 +1147,7 @@ export class AgentRuntime implements IAgentRuntime {
11471147
await Promise.all([
11481148
this.ensureUserExists(
11491149
this.agentId,
1150-
this.character.name ?? "Agent",
1150+
this.character.username ?? "Agent",
11511151
this.character.name ?? "Agent",
11521152
source
11531153
),

‎packages/core/src/types.ts

+14
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,9 @@ export type Character = {
707707
/** Optional username */
708708
username?: string;
709709

710+
/** Optional email */
711+
email?: string;
712+
710713
/** Optional system prompt */
711714
system?: string;
712715

@@ -748,6 +751,8 @@ export type Character = {
748751
telegramShouldRespondTemplate?: TemplateType;
749752
telegramAutoPostTemplate?: string;
750753
telegramPinnedMessageTemplate?: string;
754+
discordAutoPostTemplate?: string;
755+
discordAnnouncementHypeTemplate?: string;
751756
discordVoiceHandlerTemplate?: TemplateType;
752757
discordShouldRespondTemplate?: TemplateType;
753758
discordMessageHandlerTemplate?: TemplateType;
@@ -838,6 +843,14 @@ export type Character = {
838843
teamAgentIds?: string[];
839844
teamLeaderId?: string;
840845
teamMemberInterestKeywords?: string[];
846+
autoPost?: {
847+
enabled?: boolean;
848+
monitorTime?: number;
849+
inactivityThreshold?: number;
850+
mainChannelId?: string;
851+
announcementChannelIds?: string[];
852+
minTimeBetweenPosts?: number;
853+
};
841854
};
842855
telegram?: {
843856
shouldIgnoreBotMessages?: boolean;
@@ -1607,3 +1620,4 @@ export interface ChunkRow {
16071620
id: string;
16081621
// Add other properties if needed
16091622
}
1623+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { AccountService } from '../src/services/account';
3+
import { AuthenticationError } from '../src/types/internal/error';
4+
5+
// Mock the Binance client
6+
const mockAccount = vi.fn();
7+
vi.mock('@binance/connector', () => ({
8+
Spot: vi.fn().mockImplementation(() => ({
9+
account: mockAccount
10+
}))
11+
}));
12+
13+
describe('AccountService', () => {
14+
let accountService: AccountService;
15+
const mockApiKey = 'test-api-key';
16+
const mockSecretKey = 'test-secret-key';
17+
18+
beforeEach(() => {
19+
vi.clearAllMocks();
20+
});
21+
22+
describe('initialization', () => {
23+
it('should initialize with API credentials', () => {
24+
accountService = new AccountService({
25+
apiKey: mockApiKey,
26+
secretKey: mockSecretKey
27+
});
28+
expect(accountService).toBeInstanceOf(AccountService);
29+
});
30+
});
31+
32+
describe('getBalance', () => {
33+
it('should throw AuthenticationError when credentials are missing', async () => {
34+
accountService = new AccountService();
35+
await expect(accountService.getBalance({}))
36+
.rejects
37+
.toThrow(AuthenticationError);
38+
});
39+
40+
it('should filter non-zero balances', async () => {
41+
accountService = new AccountService({
42+
apiKey: mockApiKey,
43+
secretKey: mockSecretKey
44+
});
45+
46+
const mockAccountInfo = {
47+
balances: [
48+
{ asset: 'BTC', free: '1.0', locked: '0.0' },
49+
{ asset: 'ETH', free: '0.0', locked: '0.0' },
50+
{ asset: 'USDT', free: '100.0', locked: '50.0' }
51+
]
52+
};
53+
54+
mockAccount.mockResolvedValueOnce({ data: mockAccountInfo });
55+
56+
const result = await accountService.getBalance({});
57+
expect(result.balances).toHaveLength(2); // Only BTC and USDT have non-zero balances
58+
expect(result.balances).toEqual(
59+
expect.arrayContaining([
60+
expect.objectContaining({ asset: 'BTC' }),
61+
expect.objectContaining({ asset: 'USDT' })
62+
])
63+
);
64+
});
65+
66+
it('should filter by asset when specified', async () => {
67+
accountService = new AccountService({
68+
apiKey: mockApiKey,
69+
secretKey: mockSecretKey
70+
});
71+
72+
const mockAccountInfo = {
73+
balances: [
74+
{ asset: 'BTC', free: '1.0', locked: '0.0' },
75+
{ asset: 'ETH', free: '2.0', locked: '0.0' },
76+
]
77+
};
78+
79+
mockAccount.mockResolvedValueOnce({ data: mockAccountInfo });
80+
81+
const result = await accountService.getBalance({ asset: 'BTC' });
82+
expect(result.balances).toHaveLength(1);
83+
expect(result.balances[0]).toEqual(
84+
expect.objectContaining({
85+
asset: 'BTC',
86+
free: '1.0',
87+
locked: '0.0'
88+
})
89+
);
90+
});
91+
});
92+
93+
describe('checkBalance', () => {
94+
it('should return true when balance is sufficient', async () => {
95+
accountService = new AccountService({
96+
apiKey: mockApiKey,
97+
secretKey: mockSecretKey
98+
});
99+
100+
const mockAccountInfo = {
101+
balances: [
102+
{ asset: 'BTC', free: '1.0', locked: '0.0' }
103+
]
104+
};
105+
106+
mockAccount.mockResolvedValueOnce({ data: mockAccountInfo });
107+
108+
const result = await accountService.checkBalance('BTC', 0.5);
109+
expect(result).toBe(true);
110+
});
111+
112+
it('should return false when balance is insufficient', async () => {
113+
accountService = new AccountService({
114+
apiKey: mockApiKey,
115+
secretKey: mockSecretKey
116+
});
117+
118+
const mockAccountInfo = {
119+
balances: [
120+
{ asset: 'BTC', free: '0.1', locked: '0.0' }
121+
]
122+
};
123+
124+
mockAccount.mockResolvedValueOnce({ data: mockAccountInfo });
125+
126+
const result = await accountService.checkBalance('BTC', 1.0);
127+
expect(result).toBe(false);
128+
});
129+
});
130+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { PriceService } from '../src/services/price';
3+
import { BinanceError } from '../src/types/internal/error';
4+
import { ERROR_MESSAGES } from '../src/constants/errors';
5+
6+
// Mock elizaLogger
7+
vi.mock('@elizaos/core', () => ({
8+
elizaLogger: {
9+
error: vi.fn()
10+
}
11+
}));
12+
13+
describe('PriceService', () => {
14+
let service: PriceService;
15+
let mockClient: any;
16+
17+
beforeEach(() => {
18+
mockClient = {
19+
tickerPrice: vi.fn()
20+
};
21+
service = new PriceService();
22+
// @ts-ignore - we're mocking the client
23+
service.client = mockClient;
24+
});
25+
26+
describe('getPrice', () => {
27+
const validRequest = {
28+
symbol: 'BTC',
29+
quoteCurrency: 'USDT'
30+
};
31+
32+
it('should return price data for valid symbol', async () => {
33+
const mockPrice = '42150.25';
34+
mockClient.tickerPrice.mockResolvedValueOnce({
35+
data: { price: mockPrice }
36+
});
37+
38+
const result = await service.getPrice(validRequest);
39+
40+
expect(mockClient.tickerPrice).toHaveBeenCalledWith('BTCUSDT');
41+
expect(result).toEqual({
42+
symbol: 'BTCUSDT',
43+
price: mockPrice,
44+
timestamp: expect.any(Number)
45+
});
46+
});
47+
48+
it('should throw error for invalid symbol length', async () => {
49+
const invalidRequest = {
50+
symbol: 'B', // Too short
51+
quoteCurrency: 'USDT'
52+
};
53+
54+
await expect(service.getPrice(invalidRequest))
55+
.rejects
56+
.toThrow(ERROR_MESSAGES.INVALID_SYMBOL);
57+
});
58+
59+
it('should handle API errors', async () => {
60+
const apiError = new Error('API Error');
61+
mockClient.tickerPrice.mockRejectedValueOnce(apiError);
62+
63+
await expect(service.getPrice(validRequest))
64+
.rejects
65+
.toBeInstanceOf(BinanceError);
66+
});
67+
});
68+
69+
describe('formatPrice', () => {
70+
it('should format string price correctly', () => {
71+
expect(PriceService.formatPrice('42150.25')).toBe('42,150.25');
72+
expect(PriceService.formatPrice('0.00012345')).toBe('0.00012345');
73+
});
74+
75+
it('should format number price correctly', () => {
76+
expect(PriceService.formatPrice(42150.25)).toBe('42,150.25');
77+
expect(PriceService.formatPrice(0.00012345)).toBe('0.00012345');
78+
});
79+
80+
it('should handle large numbers', () => {
81+
expect(PriceService.formatPrice('1234567.89')).toBe('1,234,567.89');
82+
});
83+
84+
it('should handle small decimal numbers', () => {
85+
expect(PriceService.formatPrice('0.00000001')).toBe('0.00000001');
86+
});
87+
});
88+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { TradeService } from '../src/services/trade';
3+
import { AuthenticationError, InvalidSymbolError, MinNotionalError } from '../src/types/internal/error';
4+
import { ORDER_TYPES, TIME_IN_FORCE } from '../src/constants/api';
5+
6+
// Mock the Binance client
7+
const mockNewOrder = vi.fn();
8+
const mockExchangeInfo = vi.fn();
9+
vi.mock('@binance/connector', () => ({
10+
Spot: vi.fn().mockImplementation(() => ({
11+
newOrder: mockNewOrder,
12+
exchangeInfo: mockExchangeInfo
13+
}))
14+
}));
15+
16+
describe('TradeService', () => {
17+
let tradeService: TradeService;
18+
const mockApiKey = 'test-api-key';
19+
const mockSecretKey = 'test-secret-key';
20+
21+
beforeEach(() => {
22+
vi.clearAllMocks();
23+
tradeService = new TradeService({
24+
apiKey: mockApiKey,
25+
secretKey: mockSecretKey
26+
});
27+
});
28+
29+
describe('initialization', () => {
30+
it('should initialize with API credentials', () => {
31+
expect(tradeService).toBeInstanceOf(TradeService);
32+
});
33+
34+
it('should throw AuthenticationError when credentials are missing', async () => {
35+
tradeService = new TradeService();
36+
await expect(tradeService.executeTrade({
37+
symbol: 'BTCUSDT',
38+
side: 'BUY',
39+
type: ORDER_TYPES.MARKET,
40+
quantity: 1
41+
})).rejects.toThrow(AuthenticationError);
42+
});
43+
});
44+
45+
describe('executeTrade', () => {
46+
const mockSymbolInfo = {
47+
symbol: 'BTCUSDT',
48+
status: 'TRADING',
49+
baseAsset: 'BTC',
50+
quoteAsset: 'USDT',
51+
filters: [
52+
{
53+
filterType: 'NOTIONAL',
54+
minNotional: '10.00000000'
55+
}
56+
]
57+
};
58+
59+
const mockExchangeInfoResponse = {
60+
data: {
61+
symbols: [mockSymbolInfo]
62+
}
63+
};
64+
65+
beforeEach(() => {
66+
mockExchangeInfo.mockResolvedValue(mockExchangeInfoResponse);
67+
});
68+
69+
it('should execute a market order successfully', async () => {
70+
const mockOrderResponse = {
71+
data: {
72+
symbol: 'BTCUSDT',
73+
orderId: 12345,
74+
status: 'FILLED',
75+
executedQty: '1.0',
76+
cummulativeQuoteQty: '50000.0',
77+
price: '50000.0',
78+
type: ORDER_TYPES.MARKET,
79+
side: 'BUY'
80+
}
81+
};
82+
83+
mockNewOrder.mockResolvedValueOnce(mockOrderResponse);
84+
85+
const result = await tradeService.executeTrade({
86+
symbol: 'BTCUSDT',
87+
side: 'BUY',
88+
type: ORDER_TYPES.MARKET,
89+
quantity: 1
90+
});
91+
92+
expect(result).toEqual({
93+
symbol: 'BTCUSDT',
94+
orderId: 12345,
95+
status: 'FILLED',
96+
executedQty: '1.0',
97+
cummulativeQuoteQty: '50000.0',
98+
price: '50000.0',
99+
type: ORDER_TYPES.MARKET,
100+
side: 'BUY'
101+
});
102+
103+
expect(mockNewOrder).toHaveBeenCalledWith(
104+
'BTCUSDT',
105+
'BUY',
106+
ORDER_TYPES.MARKET,
107+
expect.objectContaining({
108+
quantity: '1'
109+
})
110+
);
111+
});
112+
113+
it('should execute a limit order successfully', async () => {
114+
const mockOrderResponse = {
115+
data: {
116+
symbol: 'BTCUSDT',
117+
orderId: 12345,
118+
status: 'NEW',
119+
executedQty: '0.0',
120+
cummulativeQuoteQty: '0.0',
121+
price: '50000.0',
122+
type: ORDER_TYPES.LIMIT,
123+
side: 'BUY'
124+
}
125+
};
126+
127+
mockNewOrder.mockResolvedValueOnce(mockOrderResponse);
128+
129+
const result = await tradeService.executeTrade({
130+
symbol: 'BTCUSDT',
131+
side: 'BUY',
132+
type: ORDER_TYPES.LIMIT,
133+
quantity: 1,
134+
price: 50000,
135+
timeInForce: TIME_IN_FORCE.GTC
136+
});
137+
138+
expect(result).toEqual({
139+
symbol: 'BTCUSDT',
140+
orderId: 12345,
141+
status: 'NEW',
142+
executedQty: '0.0',
143+
cummulativeQuoteQty: '0.0',
144+
price: '50000.0',
145+
type: ORDER_TYPES.LIMIT,
146+
side: 'BUY'
147+
});
148+
149+
expect(mockNewOrder).toHaveBeenCalledWith(
150+
'BTCUSDT',
151+
'BUY',
152+
ORDER_TYPES.LIMIT,
153+
expect.objectContaining({
154+
quantity: '1',
155+
price: '50000',
156+
timeInForce: TIME_IN_FORCE.GTC
157+
})
158+
);
159+
});
160+
161+
it('should throw error for invalid symbol', async () => {
162+
mockExchangeInfo.mockResolvedValueOnce({
163+
data: {
164+
symbols: [] // No symbols match
165+
}
166+
});
167+
168+
await expect(tradeService.executeTrade({
169+
symbol: 'INVALID',
170+
side: 'BUY',
171+
type: ORDER_TYPES.MARKET,
172+
quantity: 1
173+
})).rejects.toThrow(InvalidSymbolError);
174+
});
175+
176+
it('should throw error for insufficient notional value', async () => {
177+
// Mock successful exchange info response first
178+
mockExchangeInfo.mockResolvedValueOnce(mockExchangeInfoResponse);
179+
180+
// Mock order response with error
181+
mockNewOrder.mockRejectedValueOnce({
182+
response: {
183+
data: {
184+
code: -1013,
185+
msg: 'Filter failure: NOTIONAL'
186+
}
187+
}
188+
});
189+
190+
await expect(tradeService.executeTrade({
191+
symbol: 'BTCUSDT',
192+
side: 'BUY',
193+
type: ORDER_TYPES.MARKET,
194+
quantity: 0.0001 // Very small amount
195+
})).rejects.toThrow(MinNotionalError);
196+
});
197+
198+
it('should throw error for limit order without price', async () => {
199+
// Mock successful exchange info response
200+
mockExchangeInfo.mockResolvedValueOnce(mockExchangeInfoResponse);
201+
202+
await expect(tradeService.executeTrade({
203+
symbol: 'BTCUSDT',
204+
side: 'BUY',
205+
type: ORDER_TYPES.LIMIT,
206+
quantity: 1
207+
// price is missing
208+
})).rejects.toThrow('Price is required for LIMIT orders');
209+
});
210+
});
211+
});

‎packages/plugin-binance/package.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@
1919
"dist"
2020
],
2121
"dependencies": {
22-
"@elizaos/core": "workspace:*",
2322
"@binance/connector": "^3.6.0",
23+
"@elizaos/core": "workspace:*",
2424
"zod": "^3.22.4"
2525
},
2626
"devDependencies": {
27+
"@types/node": "^20.0.0",
2728
"tsup": "8.3.5",
28-
"@types/node": "^20.0.0"
29+
"vite-tsconfig-paths": "^5.1.4",
30+
"vitest": "^3.0.2"
2931
},
3032
"scripts": {
3133
"build": "tsup --format esm --dts",
32-
"dev": "tsup --format esm --dts --watch"
34+
"dev": "tsup --format esm --dts --watch",
35+
"test": "vitest run",
36+
"test:watch": "vitest"
3337
}
3438
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineConfig } from 'vitest/config';
2+
import { resolve } from 'path';
3+
4+
export default defineConfig({
5+
test: {
6+
globals: true,
7+
environment: 'node',
8+
testTimeout: 10000
9+
},
10+
resolve: {
11+
alias: {
12+
'@elizaos/core': resolve(__dirname, '../../packages/core/src/index.ts')
13+
},
14+
extensions: ['.ts', '.js', '.json']
15+
}
16+
});

0 commit comments

Comments
 (0)
Please sign in to comment.