Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ jobs:

- name: Run linting
run: bun run lint

- name: Check package.json
run: bun run check-packages
Binary file modified bun.lockb
Binary file not shown.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@
"lint": "eslint src/**/*.ts",
"dev": "tsx src/index.ts",
"start": "NODE_ENV=production tsx dist/index.js",
"agent": "dotenv --e /root/.agentcoin-fun/env.production -- tsx dist/index.js"
"agent": "dotenv --e /root/.agentcoin-fun/env.production -- tsx dist/index.js",
"check-packages": "bash ./scripts/check-packages.sh"
},
"dependencies": {
"@elizaos/adapter-postgres": "0.1.9",
"@elizaos/client-direct": "0.1.9",
"@elizaos/client-farcaster": "0.1.9",
"@elizaos/client-twitter": "0.1.9",
"@elizaos/core": "0.1.9",
"@elizaos/plugin-bootstrap": "0.1.9",
"@elizaos/plugin-node": "0.1.9",
"@langchain/community": "0.3.31",
"@langchain/core": "0.3.40",
"@neynar/nodejs-sdk": "2.13.1",
"@turnkey/http": "2.18.0",
"@turnkey/sdk-server": "1.7.3",
"@turnkey/viem": "0.6.10",
"agent-twitter-client": "0.0.18",
"discord.js": "14.16.3",
"amqplib": "0.10.5",
"axios": "1.7.9",
"d3-dsv": "2",
Expand Down Expand Up @@ -71,4 +73,4 @@
"tsx": "4.19.2",
"typescript": "5.6.3"
}
}
}
12 changes: 12 additions & 0 deletions scripts/check-packages.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash

# Extract dependencies and devDependencies from package.json
DEPS=$(jq -r '.dependencies, .devDependencies | to_entries[] | select(.value | test("\\^")) | "\(.key): \(.value)"' package.json)

if [ -n "$DEPS" ]; then
echo "❌ Found dependencies using caret (^):"
echo "$DEPS"
exit 1 # Fail the CI
else
echo "✅ All dependencies are locked (no caret ^ versions)."
fi
1 change: 1 addition & 0 deletions src/clients/agentcoin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@/clients/agentcoin/agentcoinfun'
58 changes: 58 additions & 0 deletions src/clients/farcaster/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { FarcasterClient } from '@/clients/farcaster/client'
import { createCastMemory } from '@/clients/farcaster/memory'
import type { Cast, CastId, Profile } from '@/clients/farcaster/types'
import { splitPostContent } from '@/clients/farcaster/utils'
import type { Content, IAgentRuntime, Memory, UUID } from '@elizaos/core'

export async function sendCast({
client,
runtime,
content,
roomId,
inReplyTo,
profile
}: {
profile: Profile
client: FarcasterClient
runtime: IAgentRuntime
content: Content
roomId: UUID
signerUuid: string
inReplyTo?: CastId
}): Promise<{ memory: Memory; cast: Cast }[]> {
const chunks = splitPostContent(content.text)
const sent: Cast[] = []
let parentCastId = inReplyTo

for (const chunk of chunks) {
const neynarCast = await client.publishCast(chunk, parentCastId)

if (neynarCast) {
const cast: Cast = {
hash: neynarCast.hash,
authorFid: neynarCast.authorFid,
text: neynarCast.text,
profile,
inReplyTo: parentCastId,
timestamp: new Date()
}

sent.push(cast)

parentCastId = {
fid: neynarCast.authorFid,
hash: neynarCast.hash
}
}
}

return sent.map((cast) => ({
cast,
memory: createCastMemory({
roomId,
senderId: runtime.agentId,
runtime,
cast
})
}))
}
249 changes: 249 additions & 0 deletions src/clients/farcaster/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import type { FarcasterConfig } from '@/clients/farcaster/environment'
import type {
Cast,
CastId,
FidRequest,
NeynarCastResponse,
NeynarCastResponseRaw,
Profile
} from '@/clients/farcaster/types'
import { type IAgentRuntime, elizaLogger } from '@elizaos/core'
import { type NeynarAPIClient, isApiErrorResponse } from '@neynar/nodejs-sdk'

export class FarcasterClient {
runtime: IAgentRuntime
neynar: NeynarAPIClient
signerUuid: string
cache: Map<string, unknown>
lastInteractionTimestamp: Date
farcasterConfig: FarcasterConfig

constructor(opts: {
runtime: IAgentRuntime
url: string
ssl: boolean
neynar: NeynarAPIClient
signerUuid: string
cache: Map<string, unknown>
farcasterConfig: FarcasterConfig
}) {
this.cache = opts.cache
this.runtime = opts.runtime
this.neynar = opts.neynar
this.signerUuid = opts.signerUuid
this.lastInteractionTimestamp = new Date()
this.farcasterConfig = opts.farcasterConfig
}

async loadCastFromNeynarResponse(neynarResponse: NeynarCastResponseRaw): Promise<Cast> {
const profile = await this.getProfile(neynarResponse.author.fid)
return {
hash: neynarResponse.hash,
authorFid: neynarResponse.author.fid,
text: neynarResponse.text,
profile,
...(neynarResponse.parent_hash
? {
inReplyTo: {
hash: neynarResponse.parent_hash,
fid: neynarResponse.parent_author.fid
}
}
: {}),
timestamp: new Date(neynarResponse.timestamp)
}
}

async publishCast(
cast: string,
parentCastId: CastId | undefined,
// eslint-disable-next-line
retryTimes?: number
): Promise<NeynarCastResponse | undefined> {
try {
const result = await this.neynar.publishCast({
signerUuid: this.signerUuid,
text: cast,
parent: parentCastId?.hash
})
if (result.success) {
return {
hash: result.cast.hash,
authorFid: result.cast.author.fid,
text: result.cast.text
}
}
} catch (err) {
if (isApiErrorResponse(err)) {
elizaLogger.error('Neynar error: ', err.response.data)
throw err.response.data
} else {
elizaLogger.error('Error: ', err)
throw err
}
}
}

async getCast(castHash: string): Promise<Cast | undefined> {
try {
if (this.cache.has(`farcaster/cast/${castHash}`)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return this.cache.get(`farcaster/cast/${castHash}`) as Cast
}

const response = await this.neynar.lookupCastByHashOrWarpcastUrl({
identifier: castHash,
type: 'hash'
})
const cast = {
hash: response.cast.hash,
authorFid: response.cast.author.fid,
text: response.cast.text,
profile: {
fid: response.cast.author.fid,
name: response.cast.author.display_name || 'anon',
username: response.cast.author.username
},
...(response.cast.parent_hash
? {
inReplyTo: {
hash: response.cast.parent_hash,
fid: response.cast.parent_author.fid
}
}
: {}),
timestamp: new Date(response.cast.timestamp)
}

this.cache.set(`farcaster/cast/${castHash}`, cast)

return cast
} catch (err) {
elizaLogger.error('Error fetching cast', err)
return undefined
}
}

async getCastsByFid(request: FidRequest): Promise<Cast[]> {
const timeline: Cast[] = []

const response = await this.neynar.fetchCastsForUser({
fid: request.fid,
limit: request.pageSize
})
response.casts.forEach((cast) => {
this.cache.set(`farcaster/cast/${cast.hash}`, cast)
timeline.push({
hash: cast.hash,
authorFid: cast.author.fid,
text: cast.text,
profile: {
fid: cast.author.fid,
name: cast.author.display_name || 'anon',
username: cast.author.username
},
timestamp: new Date(cast.timestamp)
})
})

return timeline
}

async getMentions(request: FidRequest): Promise<Cast[]> {
const neynarMentionsResponse = await this.neynar.fetchAllNotifications({
fid: request.fid,
type: ['mentions', 'replies']
})
const mentions: Cast[] = []

neynarMentionsResponse.notifications.forEach((notification) => {
const cast = {
hash: notification.cast.hash,
authorFid: notification.cast.author.fid,
text: notification.cast.text,
profile: {
fid: notification.cast.author.fid,
name: notification.cast.author.display_name || 'anon',
username: notification.cast.author.username
},
...(notification.cast.parent_hash
? {
inReplyTo: {
hash: notification.cast.parent_hash,
fid: notification.cast.parent_author.fid
}
}
: {}),
timestamp: new Date(notification.cast.timestamp)
}
mentions.push(cast)
this.cache.set(`farcaster/cast/${cast.hash}`, cast)
})

return mentions
}

async getProfile(fid: number): Promise<Profile> {
if (this.cache.has(`farcaster/profile/${fid}`)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return this.cache.get(`farcaster/profile/${fid}`) as Profile
}

const result = await this.neynar.fetchBulkUsers({ fids: [fid] })
if (!result.users || result.users.length < 1) {
elizaLogger.error('Error fetching user by fid')

throw new Error('getProfile ERROR')
}

const neynarUserProfile = result.users[0]

const profile: Profile = {
fid,
name: '',
username: ''
}

/*
const userDataBodyType = {
1: "pfp",
2: "name",
3: "bio",
5: "url",
6: "username",
// 7: "location",
// 8: "twitter",
// 9: "github",
} as const;
*/

profile.name = neynarUserProfile.display_name
profile.username = neynarUserProfile.username
profile.bio = neynarUserProfile.profile.bio.text
profile.pfp = neynarUserProfile.pfp_url

this.cache.set(`farcaster/profile/${fid}`, profile)

return profile
}

async getTimeline(request: FidRequest): Promise<{
timeline: Cast[]
nextPageToken?: Uint8Array | undefined
}> {
const timeline: Cast[] = []

const results = await this.getCastsByFid(request)

for (const cast of results) {
this.cache.set(`farcaster/cast/${cast.hash}`, cast)
timeline.push(cast)
}

return {
timeline
// TODO implement paging
// nextPageToken: results.nextPageToken,
}
}
}
Loading