Skip to content

Commit b2027f1

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into develop
* upstream/develop: test configuration and tests for client-lens (elizaOS#2534) chore(attempt): optimize pnpm cache configuration (elizaOS#2556) feat: add a way to create/store/restore agents in the filesystem (elizaOS#2389) chore: optimize pnpm cache configuration (elizaOS#2555) chore: remove cleanup step from integration tests workflow (elizaOS#2553) feat: Add more actions to Abstract Plugin (elizaOS#2531) feat: add anthropic image provider for vision (elizaOS#2524) feat: improve integrationtests workflow caching (elizaOS#2551) feat: Updated READ.me file with pre-requisites to enable telegram bot (elizaOS#2547) feat(plugin-devin): implement client-agnostic Devin plugin (elizaOS#2549) use generateObject handle undefined env variable feat:add plugin-lightning (elizaOS#2429)
2 parents bd261af + 3a69164 commit b2027f1

Some content is hidden

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

56 files changed

+5046
-583
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
@@ -31,6 +34,9 @@ DISCORD_APPLICATION_ID=
3134
DISCORD_API_TOKEN= # Bot token
3235
DISCORD_VOICE_CHANNEL_ID= # The ID of the voice channel the bot should join (optional)
3336

37+
# Devin Configuration
38+
DEVIN_API_TOKEN= # Get your API key from docs.devin.ai/tutorials/api-integration
39+
3440
# Farcaster Neynar Configuration
3541
FARCASTER_FID= # The FID associated with the account your are sending casts from
3642
FARCASTER_NEYNAR_API_KEY= # Neynar API key: https://neynar.com/
@@ -89,7 +95,7 @@ MEDIUM_OPENAI_MODEL= # Default: gpt-4o
8995
LARGE_OPENAI_MODEL= # Default: gpt-4o
9096
EMBEDDING_OPENAI_MODEL= # Default: text-embedding-3-small
9197
IMAGE_OPENAI_MODEL= # Default: dall-e-3
92-
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
9399

94100
# Community Plugin for OpenAI Configuration
95101
ENABLE_OPEN_AI_COMMUNITY_PLUGIN=false

.github/workflows/integrationTests.yaml

+10-4
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ jobs:
2222
- uses: actions/setup-node@v4
2323
with:
2424
node-version: "23.3.0"
25-
cache: "pnpm"
25+
cache: 'pnpm'
26+
cache-dependency-path: '**/pnpm-lock.yaml'
2627

27-
- name: Clean up
28-
run: pnpm clean
28+
- name: Configure pnpm
29+
run: |
30+
pnpm config set store-dir ~/.pnpm-store
31+
pnpm config set prefer-offline true
32+
pnpm config set node-linker hoisted
33+
pnpm config set shamefully-hoist true
34+
pnpm config list
2935
3036
- name: Install dependencies
31-
run: pnpm install -r --no-frozen-lockfile
37+
run: pnpm install -r --no-frozen-lockfile --prefer-offline
3238

3339
- name: Build packages
3440
run: pnpm build

agent/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,13 @@
102102
"@elizaos/plugin-hyperliquid": "workspace:*",
103103
"@elizaos/plugin-akash": "workspace:*",
104104
"@elizaos/plugin-quai": "workspace:*",
105+
"@elizaos/plugin-lightning": "workspace:*",
105106
"@elizaos/plugin-b2": "workspace:*",
106107
"@elizaos/plugin-nft-collections": "workspace:*",
107108
"@elizaos/plugin-pyth-data": "workspace:*",
108109
"@elizaos/plugin-rabbi-trader": "workspace:*",
109110
"@elizaos/plugin-openai": "workspace:*",
111+
"@elizaos/plugin-devin": "workspace:*",
110112
"rss-parser": "^3.13.0",
111113
"readline": "1.3.0",
112114
"ws": "8.18.0",

agent/src/index.ts

+47-7
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import { TelegramClientInterface } from "@elizaos/client-telegram";
1414
import { TwitterClientInterface } from "@elizaos/client-twitter";
1515
import { agentKitPlugin } from "@elizaos/plugin-agentkit";
1616
// import { ReclaimAdapter } from "@elizaos/plugin-reclaim";
17-
import { onchainJson } from "@elizaos/plugin-iq6900";
1817
import { PrimusAdapter } from "@elizaos/plugin-primus";
18+
import { lightningPlugin } from "@elizaos/plugin-lightning";
19+
import { elizaCodeinPlugin, onchainJson } from "@elizaos/plugin-iq6900";
1920

2021
import {
2122
AgentRuntime,
@@ -109,6 +110,15 @@ import { verifiableLogPlugin } from "@elizaos/plugin-tee-verifiable-log";
109110
import { thirdwebPlugin } from "@elizaos/plugin-thirdweb";
110111
import { tonPlugin } from "@elizaos/plugin-ton";
111112
import { webSearchPlugin } from "@elizaos/plugin-web-search";
113+
import { injectivePlugin } from "@elizaos/plugin-injective";
114+
import { giphyPlugin } from "@elizaos/plugin-giphy";
115+
import { letzAIPlugin } from "@elizaos/plugin-letzai";
116+
import { thirdwebPlugin } from "@elizaos/plugin-thirdweb";
117+
import { hyperliquidPlugin } from "@elizaos/plugin-hyperliquid";
118+
import { echoChambersPlugin } from "@elizaos/plugin-echochambers";
119+
import { dexScreenerPlugin } from "@elizaos/plugin-dexscreener";
120+
import { pythDataPlugin } from "@elizaos/plugin-pyth-data";
121+
import { openaiPlugin } from "@elizaos/plugin-openai";
112122

113123
import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era";
114124
import Database from "better-sqlite3";
@@ -390,10 +400,32 @@ function commaSeparatedStringToArray(commaSeparated: string): string[] {
390400
return commaSeparated?.split(",").map((value) => value.trim());
391401
}
392402

403+
async function readCharactersFromStorage(
404+
characterPaths: string[]
405+
): Promise<string[]> {
406+
try {
407+
const uploadDir = path.join(process.cwd(), "data", "characters");
408+
await fs.promises.mkdir(uploadDir, { recursive: true });
409+
const fileNames = await fs.promises.readdir(uploadDir);
410+
fileNames.forEach((fileName) => {
411+
characterPaths.push(path.join(uploadDir, fileName));
412+
});
413+
} catch (err) {
414+
elizaLogger.error(`Error reading directory: ${err.message}`);
415+
}
416+
417+
return characterPaths;
418+
}
419+
393420
export async function loadCharacters(
394421
charactersArg: string
395422
): Promise<Character[]> {
396-
const characterPaths = commaSeparatedStringToArray(charactersArg);
423+
let characterPaths = commaSeparatedStringToArray(charactersArg);
424+
425+
if (process.env.USE_CHARACTER_STORAGE === "true") {
426+
characterPaths = await readCharactersFromStorage(characterPaths);
427+
}
428+
397429
const loadedCharacters: Character[] = [];
398430

399431
if (characterPaths?.length > 0) {
@@ -1079,14 +1111,20 @@ export async function createAgent(
10791111
getSecret(character, "RESERVOIR_API_KEY")
10801112
? createNFTCollectionsPlugin()
10811113
: null,
1082-
// getSecret(character, "PYTH_TESTNET_PROGRAM_KEY") ||
1083-
// getSecret(character, "PYTH_MAINNET_PROGRAM_KEY")
1084-
// ? pythDataPlugin
1085-
// : null,
1114+
getSecret(character, "PYTH_TESTNET_PROGRAM_KEY") ||
1115+
getSecret(character, "PYTH_MAINNET_PROGRAM_KEY")
1116+
? pythDataPlugin
1117+
: null,
1118+
getSecret(character, "LND_TLS_CERT") &&
1119+
getSecret(character, "LND_MACAROON") &&
1120+
getSecret(character, "LND_SOCKET")
1121+
? lightningPlugin
1122+
: null,
10861123
getSecret(character, "OPENAI_API_KEY") &&
10871124
getSecret(character, "ENABLE_OPEN_AI_COMMUNITY_PLUGIN")
10881125
? openaiPlugin
10891126
: null,
1127+
getSecret(character, "DEVIN_API_TOKEN") ? devinPlugin : null,
10901128
].filter(Boolean),
10911129
providers: [],
10921130
actions: [],
@@ -1264,7 +1302,9 @@ const startAgents = async () => {
12641302
characters = await loadCharacterFromOnchain();
12651303
}
12661304

1267-
if ((!onchainJson && charactersArg) || hasValidRemoteUrls()) {
1305+
const notOnchainJson = !onchainJson || onchainJson == "null";
1306+
1307+
if ((notOnchainJson && charactersArg) || hasValidRemoteUrls()) {
12681308
characters = await loadCharacters(charactersArg);
12691309
}
12701310

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,124 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { LensClient } from '../src/client';
3+
import { LensClient as LensClientCore, LimitType, PublicationType } from '@lens-protocol/client';
4+
5+
// Mock dependencies
6+
vi.mock('@lens-protocol/client', async () => {
7+
const actual = await vi.importActual('@lens-protocol/client');
8+
return {
9+
...actual,
10+
LensClient: vi.fn().mockImplementation(() => ({
11+
authentication: {
12+
generateChallenge: vi.fn().mockResolvedValue({ id: 'challenge-id', text: 'challenge-text' }),
13+
authenticate: vi.fn().mockResolvedValue({ accessToken: 'mock-token', refreshToken: 'mock-refresh' })
14+
},
15+
profile: {
16+
fetch: vi.fn().mockResolvedValue({
17+
id: '0x01',
18+
handle: { localName: 'test.lens' },
19+
metadata: {
20+
displayName: 'Test User',
21+
bio: 'Test bio',
22+
picture: {
23+
uri: 'https://example.com/pic-raw.jpg'
24+
}
25+
}
26+
})
27+
},
28+
publication: {
29+
fetchAll: vi.fn().mockResolvedValue({
30+
items: [
31+
{
32+
id: 'pub-1',
33+
metadata: { content: 'Test post' },
34+
stats: { reactions: 10 }
35+
}
36+
]
37+
})
38+
}
39+
}))
40+
};
41+
});
42+
43+
describe('LensClient', () => {
44+
let client: LensClient;
45+
const mockRuntime = {
46+
name: 'test-runtime',
47+
memory: new Map(),
48+
getMemory: vi.fn(),
49+
setMemory: vi.fn(),
50+
clearMemory: vi.fn()
51+
};
52+
const mockAccount = {
53+
address: '0x123' as `0x${string}`,
54+
privateKey: '0xabc' as `0x${string}`,
55+
signMessage: vi.fn().mockResolvedValue('signed-message'),
56+
signTypedData: vi.fn()
57+
};
58+
59+
beforeEach(() => {
60+
vi.clearAllMocks();
61+
client = new LensClient({
62+
runtime: mockRuntime,
63+
cache: new Map(),
64+
account: mockAccount,
65+
profileId: '0x01' as `0x${string}`
66+
});
67+
});
68+
69+
describe('authenticate', () => {
70+
it('should authenticate successfully', async () => {
71+
await client.authenticate();
72+
expect(client['authenticated']).toBe(true);
73+
expect(client['core'].authentication.generateChallenge).toHaveBeenCalledWith({
74+
signedBy: mockAccount.address,
75+
for: '0x01'
76+
});
77+
expect(mockAccount.signMessage).toHaveBeenCalledWith({ message: 'challenge-text' });
78+
});
79+
80+
it('should handle authentication errors', async () => {
81+
const mockError = new Error('Auth failed');
82+
vi.mocked(client['core'].authentication.generateChallenge).mockRejectedValueOnce(mockError);
83+
84+
await expect(client.authenticate()).rejects.toThrow('Auth failed');
85+
expect(client['authenticated']).toBe(false);
86+
});
87+
});
88+
89+
describe('getPublicationsFor', () => {
90+
it('should fetch publications successfully', async () => {
91+
const publications = await client.getPublicationsFor('0x123');
92+
expect(publications).toHaveLength(1);
93+
expect(publications[0].id).toBe('pub-1');
94+
expect(client['core'].publication.fetchAll).toHaveBeenCalledWith({
95+
limit: LimitType.Fifty,
96+
where: {
97+
from: ['0x123'],
98+
publicationTypes: [PublicationType.Post]
99+
}
100+
});
101+
});
102+
103+
it('should handle fetch errors', async () => {
104+
vi.mocked(client['core'].publication.fetchAll).mockRejectedValueOnce(new Error('Fetch failed'));
105+
await expect(client.getPublicationsFor('0x123')).rejects.toThrow('Fetch failed');
106+
});
107+
});
108+
109+
describe('getProfile', () => {
110+
it('should fetch profile successfully', async () => {
111+
const profile = await client.getProfile('0x123');
112+
expect(profile).toBeDefined();
113+
expect(profile.id).toBe('0x01');
114+
expect(profile.handle).toBe('test.lens');
115+
expect(profile.pfp).toBe('https://example.com/pic-raw.jpg');
116+
expect(client['core'].profile.fetch).toHaveBeenCalledWith({ forProfileId: '0x123' });
117+
});
118+
119+
it('should handle profile fetch errors', async () => {
120+
vi.mocked(client['core'].profile.fetch).mockRejectedValueOnce(new Error('Profile fetch failed'));
121+
await expect(client.getProfile('0x123')).rejects.toThrow('Profile fetch failed');
122+
});
123+
});
124+
});

0 commit comments

Comments
 (0)