Skip to content

Commit 9573e6f

Browse files
authored
Merge branch 'develop' into develop
2 parents 6a04ab7 + 0d986f6 commit 9573e6f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2881
-299
lines changed

.env.example

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ SUPABASE_ANON_KEY=
1818
# Comma separated list of remote character urls (optional)
1919
REMOTE_CHARACTER_URLS=
2020

21+
# Stores characters set by using the direct API in the data/character folder for further load when the app restarts
22+
USE_CHARACTER_STORAGE=false
23+
2124
# Logging
2225
DEFAULT_LOG_LEVEL=warn
2326
LOG_JSON_FORMAT=false # Print everything in logger as json; false by default
@@ -92,7 +95,7 @@ MEDIUM_OPENAI_MODEL= # Default: gpt-4o
9295
LARGE_OPENAI_MODEL= # Default: gpt-4o
9396
EMBEDDING_OPENAI_MODEL= # Default: text-embedding-3-small
9497
IMAGE_OPENAI_MODEL= # Default: dall-e-3
95-
USE_OPENAI_EMBEDDING= # Set to TRUE for OpenAI/1536, leave blank for local
98+
USE_OPENAI_EMBEDDING=TRUE # Set to TRUE for OpenAI/1536, leave blank for local
9699

97100
# Community Plugin for OpenAI Configuration
98101
ENABLE_OPEN_AI_COMMUNITY_PLUGIN=false
@@ -491,6 +494,9 @@ NEAR_NETWORK=testnet # or mainnet
491494
ZKSYNC_ADDRESS=
492495
ZKSYNC_PRIVATE_KEY=
493496

497+
# HoldStation Wallet Configuration
498+
HOLDSTATION_PRIVATE_KEY=
499+
494500
# Avail DA Configuration
495501
AVAIL_ADDRESS=
496502
AVAIL_SEED=
+13-25
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
name: Integration Tests
22
on:
33
push:
4-
branches:
5-
- "*"
4+
branches: ["*"]
65
pull_request:
7-
branches:
8-
- "*"
6+
branches: ["*"]
97

108
jobs:
119
integration-tests:
@@ -15,6 +13,11 @@ jobs:
1513
steps:
1614
- uses: actions/checkout@v4
1715

16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '23.3'
20+
1821
- name: Cache pnpm
1922
uses: actions/cache@v4
2023
with:
@@ -23,28 +26,14 @@ jobs:
2326
**/node_modules
2427
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
2528
restore-keys: ${{ runner.os }}-pnpm-
26-
27-
- name: Cache plugin dist
28-
uses: actions/cache@v4
29-
with:
30-
path: |
31-
**/packages/plugin-*/dist
32-
key: ${{ runner.os }}-plugin-dist-${{ hashFiles('**/pnpm-lock.yaml') }}
33-
restore-keys: ${{ runner.os }}-plugin-dist-
34-
35-
- uses: pnpm/action-setup@v3
29+
30+
- name: Setup pnpm
31+
uses: pnpm/action-setup@v3
3632
with:
3733
version: 9.15.0
38-
39-
- uses: actions/setup-node@v4
40-
with:
41-
node-version: "23.3.0"
42-
43-
- name: Clean up
44-
run: pnpm clean
45-
34+
4635
- name: Install dependencies
47-
run: pnpm install -r --no-frozen-lockfile
36+
run: pnpm install --no-frozen-lockfile
4837

4938
- name: Build packages
5039
run: pnpm build
@@ -53,5 +42,4 @@ jobs:
5342
env:
5443
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
5544
COINBASE_COMMERCE_KEY: ${{ secrets.COINBASE_COMMERCE_KEY }}
56-
run: |
57-
pnpm run integrationTests
45+
run: pnpm run integrationTests

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ concatenated-output.ts
99
embedding-cache.json
1010
packages/plugin-buttplug/intiface-engine
1111

12+
node-compile-cache
13+
1214
.idea
1315
.DS_Store
1416

agent/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"@elizaos/plugin-pyth-data": "workspace:*",
109109
"@elizaos/plugin-openai": "workspace:*",
110110
"@elizaos/plugin-devin": "workspace:*",
111+
"@elizaos/plugin-holdstation": "workspace:*",
111112
"readline": "1.3.0",
112113
"ws": "8.18.0",
113114
"yargs": "17.7.2"

agent/src/index.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { agentKitPlugin } from "@elizaos/plugin-agentkit";
1717
import { PrimusAdapter } from "@elizaos/plugin-primus";
1818
import { lightningPlugin } from "@elizaos/plugin-lightning";
1919
import { elizaCodeinPlugin, onchainJson } from "@elizaos/plugin-iq6900";
20+
import { holdstationPlugin } from "@elizaos/plugin-holdstation";
2021

2122
import {
2223
AgentRuntime,
@@ -390,10 +391,31 @@ function commaSeparatedStringToArray(commaSeparated: string): string[] {
390391
return commaSeparated?.split(",").map((value) => value.trim());
391392
}
392393

394+
async function readCharactersFromStorage(characterPaths: string[]): Promise<string[]> {
395+
try {
396+
const uploadDir = path.join(process.cwd(), "data", "characters");
397+
await fs.promises.mkdir(uploadDir, { recursive: true });
398+
const fileNames = await fs.promises.readdir(uploadDir);
399+
fileNames.forEach(fileName => {
400+
characterPaths.push(path.join(uploadDir, fileName));
401+
});
402+
} catch (err) {
403+
elizaLogger.error(`Error reading directory: ${err.message}`);
404+
}
405+
406+
return characterPaths;
407+
};
408+
393409
export async function loadCharacters(
394410
charactersArg: string
395411
): Promise<Character[]> {
396-
const characterPaths = commaSeparatedStringToArray(charactersArg);
412+
413+
let characterPaths = commaSeparatedStringToArray(charactersArg);
414+
415+
if(process.env.USE_CHARACTER_STORAGE === "true") {
416+
characterPaths = await readCharactersFromStorage(characterPaths);
417+
}
418+
397419
const loadedCharacters: Character[] = [];
398420

399421
if (characterPaths?.length > 0) {
@@ -1067,12 +1089,15 @@ export async function createAgent(
10671089
? lightningPlugin
10681090
: null,
10691091
getSecret(character, "OPENAI_API_KEY") &&
1070-
getSecret(character, "ENABLE_OPEN_AI_COMMUNITY_PLUGIN")
1092+
parseBooleanFromText(getSecret(character, "ENABLE_OPEN_AI_COMMUNITY_PLUGIN"))
10711093
? openaiPlugin
10721094
: null,
10731095
getSecret(character, "DEVIN_API_TOKEN")
10741096
? devinPlugin
10751097
: null,
1098+
getSecret(character, "HOLDSTATION_PRIVATE_KEY")
1099+
? holdstationPlugin
1100+
: null,
10761101
].filter(Boolean),
10771102
providers: [],
10781103
actions: [],
@@ -1250,7 +1275,9 @@ const startAgents = async () => {
12501275
characters = await loadCharacterFromOnchain();
12511276
}
12521277

1253-
if ((!onchainJson && charactersArg) || hasValidRemoteUrls()) {
1278+
const notOnchainJson = !onchainJson || onchainJson == "null";
1279+
1280+
if ((notOnchainJson && charactersArg) || hasValidRemoteUrls()) {
12541281
characters = await loadCharacters(charactersArg);
12551282
}
12561283

packages/client-direct/src/api.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import express from "express";
22
import bodyParser from "body-parser";
33
import cors from "cors";
4+
import path from "path";
5+
import fs from "fs";
46

57
import {
68
type AgentRuntime,
@@ -80,6 +82,16 @@ export function createApiRouter(
8082
res.json({ agents: agentsList });
8183
});
8284

85+
router.get('/storage', async (req, res) => {
86+
try {
87+
const uploadDir = path.join(process.cwd(), "data", "characters");
88+
const files = await fs.promises.readdir(uploadDir);
89+
res.json({ files });
90+
} catch (error) {
91+
res.status(500).json({ error: error.message });
92+
}
93+
});
94+
8395
router.get("/agents/:agentId", (req, res) => {
8496
const { agentId } = validateUUIDParams(req.params, res) ?? {
8597
agentId: null,
@@ -127,7 +139,7 @@ export function createApiRouter(
127139
};
128140
if (!agentId) return;
129141

130-
const agent: AgentRuntime = agents.get(agentId);
142+
let agent: AgentRuntime = agents.get(agentId);
131143

132144
// update character
133145
if (agent) {
@@ -137,6 +149,9 @@ export function createApiRouter(
137149
// if it has a different name, the agentId will change
138150
}
139151

152+
// stores the json data before it is modified with added data
153+
const characterJson = { ...req.body };
154+
140155
// load character from body
141156
const character = req.body;
142157
try {
@@ -152,7 +167,7 @@ export function createApiRouter(
152167

153168
// start it up (and register it)
154169
try {
155-
await directClient.startAgent(character);
170+
agent = await directClient.startAgent(character);
156171
elizaLogger.log(`${character.name} started`);
157172
} catch (e) {
158173
elizaLogger.error(`Error starting agent: ${e}`);
@@ -162,6 +177,35 @@ export function createApiRouter(
162177
});
163178
return;
164179
}
180+
181+
if (process.env.USE_CHARACTER_STORAGE === "true") {
182+
try {
183+
const filename = `${agent.agentId}.json`;
184+
const uploadDir = path.join(
185+
process.cwd(),
186+
"data",
187+
"characters"
188+
);
189+
const filepath = path.join(uploadDir, filename);
190+
await fs.promises.mkdir(uploadDir, { recursive: true });
191+
await fs.promises.writeFile(
192+
filepath,
193+
JSON.stringify(
194+
{ ...characterJson, id: agent.agentId },
195+
null,
196+
2
197+
)
198+
);
199+
elizaLogger.info(
200+
`Character stored successfully at ${filepath}`
201+
);
202+
} catch (error) {
203+
elizaLogger.error(
204+
`Failed to store character: ${error.message}`
205+
);
206+
}
207+
}
208+
165209
res.json({
166210
id: character.id,
167211
character: character,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { createTestCast } from './test-utils';
3+
import { FarcasterClient } from '../src/client';
4+
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
5+
6+
// Mock dependencies
7+
vi.mock('@neynar/nodejs-sdk', () => ({
8+
NeynarAPIClient: vi.fn().mockImplementation(() => ({
9+
publishCast: vi.fn().mockResolvedValue({
10+
success: true,
11+
cast: {
12+
hash: 'cast-1',
13+
author: { fid: '123' },
14+
text: 'Test cast',
15+
timestamp: '2025-01-20T20:00:00Z'
16+
}
17+
}),
18+
fetchBulkUsers: vi.fn().mockResolvedValue({
19+
users: [{
20+
fid: '123',
21+
username: 'test.farcaster',
22+
display_name: 'Test User',
23+
pfp: {
24+
url: 'https://example.com/pic.jpg'
25+
}
26+
}]
27+
})
28+
}))
29+
}));
30+
31+
describe('Cast Functions', () => {
32+
let client: FarcasterClient;
33+
34+
beforeEach(() => {
35+
vi.clearAllMocks();
36+
client = new FarcasterClient({
37+
runtime: {
38+
name: 'test-runtime',
39+
memory: new Map(),
40+
getMemory: vi.fn(),
41+
setMemory: vi.fn(),
42+
clearMemory: vi.fn()
43+
},
44+
url: 'https://api.example.com',
45+
ssl: true,
46+
neynar: new NeynarAPIClient({ apiKey: 'test-key' }),
47+
signerUuid: 'test-signer',
48+
cache: new Map(),
49+
farcasterConfig: {
50+
apiKey: 'test-key',
51+
signerUuid: 'test-signer'
52+
}
53+
});
54+
});
55+
56+
describe('createTestCast', () => {
57+
it('should create a cast successfully', async () => {
58+
const content = 'Test cast content';
59+
const result = await createTestCast(client, content);
60+
61+
expect(result).toBeDefined();
62+
expect(result.success).toBe(true);
63+
expect(result.cast.text).toBe(content);
64+
expect(client.neynar.publishCast).toHaveBeenCalledWith({
65+
text: content,
66+
signerUuid: 'test-signer'
67+
});
68+
});
69+
70+
it('should handle cast creation errors', async () => {
71+
const content = 'Test cast content';
72+
vi.mocked(client.neynar.publishCast).mockRejectedValueOnce(new Error('Cast creation failed'));
73+
await expect(createTestCast(client, content)).rejects.toThrow('Cast creation failed');
74+
});
75+
76+
it('should handle empty content', async () => {
77+
const content = '';
78+
await expect(createTestCast(client, content)).rejects.toThrow('Cast content cannot be empty');
79+
});
80+
81+
it('should handle very long content', async () => {
82+
const content = 'a'.repeat(321); // Farcaster limit is 320 characters
83+
await expect(createTestCast(client, content)).rejects.toThrow('Cast content too long');
84+
});
85+
});
86+
});

0 commit comments

Comments
 (0)