diff --git a/core-concepts/plugins.mdx b/core-concepts/plugins.mdx index 536d65e..5ab2e74 100644 --- a/core-concepts/plugins.mdx +++ b/core-concepts/plugins.mdx @@ -36,7 +36,7 @@ export const myPlugin: Plugin = { ## Core Plugin: Bootstrap -Every agent includes `@elizaos/plugin-bootstrap` which provides essential functionality for message handling, knowledge management, and basic agent operations. For detailed information, see the [Bootstrap Plugin Deep Dive](/plugins/bootstrap/complete-documentation). +Every agent includes `@elizaos/plugin-bootstrap` which provides essential functionality for message handling, knowledge management, and basic agent operations. For detailed information, see the [Bootstrap Plugin Deep Dive](/plugins/bootstrap/complete-documentation.mdx). ## Platform Plugins diff --git a/docs.json b/docs.json index 64f7c25..757a7d0 100644 --- a/docs.json +++ b/docs.json @@ -327,6 +327,16 @@ "plugins/platform/twitter/examples", "plugins/platform/twitter/testing-guide" ] + }, + { + "group": "Farcaster", + "pages": [ + "plugins/platform/farcaster", + "plugins/platform/farcaster/developer-guide", + "plugins/platform/farcaster/cast-flow", + "plugins/platform/farcaster/examples", + "plugins/platform/farcaster/testing-guide" + ] } ] }, diff --git a/guides/plugin-migration/advanced-migration-guide.mdx b/guides/plugin-migration/advanced-migration-guide.mdx index 916228e..b0db385 100644 --- a/guides/plugin-migration/advanced-migration-guide.mdx +++ b/guides/plugin-migration/advanced-migration-guide.mdx @@ -4,7 +4,7 @@ description: "Advanced breaking changes for evaluators, services, and runtime me icon: "code" --- -> **Important**: This guide covers advanced breaking changes for evaluators, services, and runtime methods. Read the main [migration guide](./migration-guide) first for actions, providers, and basic migrations. +> **Important**: This guide covers advanced breaking changes for evaluators, services, and runtime methods. Read the main [migration guide](./migration-guide.mdx) first for actions, providers, and basic migrations. ## Table of Contents @@ -458,6 +458,6 @@ class MyClient extends Service { If you encounter issues not covered in this guide: -1. Check the main [migration guide](./migration-guide) for basic migrations +1. Check the main [migration guide](./migration-guide.mdx) for basic migrations 2. Review the v1.x examples in the elizaOS repository for reference implementations 3. Join our Discord community for support diff --git a/guides/plugin-migration/overview.mdx b/guides/plugin-migration/overview.mdx index 5df5f31..4e7eb9d 100644 --- a/guides/plugin-migration/overview.mdx +++ b/guides/plugin-migration/overview.mdx @@ -10,7 +10,7 @@ This comprehensive guide will walk you through migrating your elizaOS plugins fr Follow these guides in order for a smooth migration: -### 1. [Migration Overview](./migration-guide) +### 1. [Migration Overview](./migration-guide.mdx) Start here! This guide covers: - Key differences between 0.x and 1.x - Breaking changes and new features @@ -130,4 +130,4 @@ The 1.x architecture brings: - **Better performance** - Optimized runtime execution - **Stronger typing** - Catch errors at compile time -Start with the [Migration Overview](./migration-guide) and work through each guide systematically for the best results! \ No newline at end of file +Start with the [Migration Overview](./migration-guide.mdx) and work through each guide systematically for the best results! \ No newline at end of file diff --git a/images/icons/farcaster.svg b/images/icons/farcaster.svg new file mode 100644 index 0000000..d685482 --- /dev/null +++ b/images/icons/farcaster.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/bootstrap.mdx b/plugins/bootstrap.mdx index 2dd482e..167d500 100644 --- a/plugins/bootstrap.mdx +++ b/plugins/bootstrap.mdx @@ -10,16 +10,16 @@ Welcome to the comprehensive documentation for the `@elizaos/plugin-bootstrap` p ### Core Documentation -- **[Complete Developer Documentation](/plugins/bootstrap/complete-documentation)** +- **[Complete Developer Documentation](/plugins/bootstrap/complete-documentation.mdx)** Comprehensive guide covering all components, architecture, and implementation details -- **[Message Flow Diagram](/plugins/bootstrap/message-flow)** +- **[Message Flow Diagram](/plugins/bootstrap/message-flow.mdx)** Step-by-step breakdown of how messages flow through the system with visual diagrams -- **[Examples & Recipes](/plugins/bootstrap/examples)** +- **[Examples & Recipes](/plugins/bootstrap/examples.mdx)** Practical examples, code snippets, and real-world implementations -- **[Testing Guide](/plugins/bootstrap/testing-guide)** +- **[Testing Guide](/plugins/bootstrap/testing-guide.mdx)** Testing patterns, best practices, and comprehensive test examples diff --git a/plugins/defi/evm.mdx b/plugins/defi/evm.mdx index 9bb6311..3e8f03c 100644 --- a/plugins/defi/evm.mdx +++ b/plugins/defi/evm.mdx @@ -142,7 +142,7 @@ The plugin includes comprehensive error handling for common scenarios: ## Next Steps -- [Complete Documentation →](./evm/complete-documentation) -- [DeFi Operations Flow →](./evm/defi-operations-flow) -- [Examples →](./evm/examples) -- [Testing Guide →](./evm/testing-guide) +- [Complete Documentation →](./evm/complete-documentation.mdx) +- [DeFi Operations Flow →](./evm/defi-operations-flow.mdx) +- [Examples →](./evm/examples.mdx) +- [Testing Guide →](./evm/testing-guide.mdx) diff --git a/plugins/defi/solana.mdx b/plugins/defi/solana.mdx index fc53ce7..56729a2 100644 --- a/plugins/defi/solana.mdx +++ b/plugins/defi/solana.mdx @@ -162,6 +162,6 @@ The plugin includes robust error handling for: ## Next Steps - [Complete Documentation →](./solana/complete-documentation.mdx) -- [DeFi Operations Flow →](./solana/defi-operations-flow) -- [Examples →](./solana/examples) -- [Testing Guide →](./solana/testing-guide) +- [DeFi Operations Flow →](./solana/defi-operations-flow.mdx) +- [Examples →](./solana/examples.mdx) +- [Testing Guide →](./solana/testing-guide.mdx) diff --git a/plugins/llm.mdx b/plugins/llm.mdx index 0692ac9..ebdca48 100644 --- a/plugins/llm.mdx +++ b/plugins/llm.mdx @@ -232,14 +232,14 @@ You can also configure API keys per character: ### Cloud Providers -- [OpenAI Plugin](./openai) - Full-featured with all model types -- [Anthropic Plugin](./anthropic) - Claude models for text generation -- [Google GenAI Plugin](./google-genai) - Gemini models -- [OpenRouter Plugin](./openrouter) - Access to multiple providers +- [OpenAI Plugin](./openai.mdx) - Full-featured with all model types +- [Anthropic Plugin](./anthropic.mdx) - Claude models for text generation +- [Google GenAI Plugin](./google-genai.mdx) - Gemini models +- [OpenRouter Plugin](./openrouter.mdx) - Access to multiple providers ### Local/Self-Hosted -- [Ollama Plugin](./ollama) - Run models locally with Ollama +- [Ollama Plugin](./ollama.mdx) - Run models locally with Ollama ## Best Practices diff --git a/plugins/overview.mdx b/plugins/overview.mdx index 6051a82..06390af 100644 --- a/plugins/overview.mdx +++ b/plugins/overview.mdx @@ -44,7 +44,7 @@ Blockchain and DeFi integrations for Web3 functionality: Connect your agent to popular platforms: - + Full Discord integration with voice, commands, and rich interactions. @@ -56,6 +56,10 @@ Connect your agent to popular platforms: Twitter/X integration for posting, replying, and timeline management. + + + Farcaster social network integration with casting and engagement. + ## LLM Providers diff --git a/plugins/platform/farcaster.mdx b/plugins/platform/farcaster.mdx new file mode 100644 index 0000000..cd3a555 --- /dev/null +++ b/plugins/platform/farcaster.mdx @@ -0,0 +1,29 @@ +--- +title: "Farcaster Integration" +description: "Welcome to the comprehensive documentation for the @elizaos/plugin-farcaster package. This index provides organized access to all documentation resources." +--- + + +The @elizaos/plugin-farcaster enables your elizaOS agent to interact with the Farcaster social network through casting, replying, and engaging with the decentralized social protocol. + +## 📚 Documentation + +- **[Developer Guide](./farcaster/developer-guide.mdx)** - Detailed technical reference +- **[Cast Flow](./farcaster/cast-flow.mdx)** - Visual guide to cast processing +- **[Examples](./farcaster/examples.mdx)** - Practical implementation examples +- **[Testing Guide](./farcaster/testing-guide.mdx)** - Testing strategies and patterns + +## 🔧 Configuration + +### Required Settings +- `FARCASTER_NEYNAR_API_KEY` - Neynar API key for authentication +- `FARCASTER_SIGNER_UUID` - Neynar signer UUID for your account +- `FARCASTER_FID` - Your Farcaster ID (FID) + +### Optional Settings +- `ENABLE_CAST` - Enable autonomous casting (default: true) +- `ENABLE_ACTION_PROCESSING` - Enable processing interactions (default: false) +- `FARCASTER_DRY_RUN` - Test mode without posting (default: false) +- `CAST_INTERVAL_MIN` - Minimum interval between casts in minutes (default: 90) +- `CAST_INTERVAL_MAX` - Maximum interval between casts in minutes (default: 180) +- `ACTION_TIMELINE_TYPE` - Type of timeline to use for actions (default: ForYou) diff --git a/plugins/platform/farcaster/cast-flow.mdx b/plugins/platform/farcaster/cast-flow.mdx new file mode 100644 index 0000000..5839033 --- /dev/null +++ b/plugins/platform/farcaster/cast-flow.mdx @@ -0,0 +1,330 @@ +--- +title: "Cast Flow" +description: "Visual guide to understanding how the Farcaster plugin processes casts and interactions" +--- + +# Farcaster Cast Flow + +## Overview + +This document provides a visual and detailed explanation of how the Farcaster plugin processes casts, from initial receipt through evaluation, response generation, and posting. + +## Cast Processing Pipeline + +```mermaid +graph TD + A[Neynar API Polling] --> B{Event Type} + B -->|New Cast| C[Cast Processor] + B -->|Reply| D[Reply Handler] + B -->|Mention| E[Mention Handler] + B -->|Timeline Update| F[Timeline Handler] + + C --> G[Content Analysis] + D --> G + E --> G + + G --> H{Should Respond?} + H -->|Yes| I[Generate Response] + H -->|No| J[Store & Skip] + + I --> K[Format Cast] + K --> L[Neynar API Call] + L --> M[Submit Cast] + M --> N[Store Result] + + F --> O[Update Context] + J --> O + N --> O +``` + +## Detailed Flow Stages + +### 1. Event Reception + +The plugin polls the Neynar API for relevant events and interactions: + +```typescript +// Neynar API polling for mentions and timeline +setInterval(async () => { + const mentions = await neynarClient.fetchMentions({ + fid: agentFid, + limit: 10 + }); + + const timeline = await neynarClient.fetchTimeline({ + fid: agentFid, + type: 'ForYou' + }); + + await processEvents(mentions, timeline); +}, FARCASTER_POLL_INTERVAL * 60000); +``` + +### 2. Event Classification + +Events are classified and routed to appropriate handlers: + +```mermaid +graph LR + A[Incoming Event] --> B{Classification} + B --> C[Direct Mention] + B --> D[Channel Cast] + B --> E[Reply Thread] + B --> F[Timeline Cast] + + C --> G[Priority Queue] + D --> H[Channel Handler] + E --> I[Thread Handler] + F --> J[Timeline Handler] +``` + +### 3. Content Analysis + +Each cast undergoes multi-stage analysis: + +```mermaid +graph TD + A[Cast Content] --> B[Tokenization] + B --> C[Sentiment Analysis] + C --> D[Topic Extraction] + D --> E[Context Building] + E --> F[Relevance Scoring] + F --> G{Score Threshold} + G -->|High| H[Immediate Response] + G -->|Medium| I[Queue for Response] + G -->|Low| J[Monitor Only] +``` + +### 4. Response Decision Tree + +```mermaid +graph TD + A[Cast Received] --> B{Is Mention?} + B -->|Yes| C[High Priority Response] + B -->|No| D{Is Reply to Agent?} + D -->|Yes| E[Continue Conversation] + D -->|No| F{Contains Keywords?} + F -->|Yes| G{Sentiment Check} + F -->|No| H[No Response] + G -->|Positive| I[Engage Positively] + G -->|Negative| J[Careful Response] + G -->|Neutral| K{Random Engagement} + K -->|Yes| L[Generate Response] + K -->|No| H +``` + +### 5. Response Generation + +The response generation process: + +```typescript +async function generateResponse(context: CastContext): Promise { + // 1. Build conversation history + const thread = await getThreadContext(context.parentHash); + + // 2. Extract key topics + const topics = extractTopics(context.text); + + // 3. Generate appropriate response + const response = await llm.generate({ + system: character.personality, + context: thread, + topics: topics, + maxLength: 320 + }); + + // 4. Validate and format + return formatCast(response); +} +``` + +### 6. Cast Composition + +```mermaid +graph TD + A[Generated Text] --> B{Length Check} + B -->|Over 320| C[Truncate/Split] + B -->|Under 320| D[Format Check] + C --> D + D --> E{Has Embeds?} + E -->|Yes| F[Validate URLs] + E -->|No| G[Add Metadata] + F --> G + G --> H{Channel Cast?} + H -->|Yes| I[Add Channel Tag] + H -->|No| J[Standard Cast] + I --> K[Final Cast Object] + J --> K +``` + +### 7. Cast Publishing via Neynar + +```mermaid +graph LR + A[Cast Object] --> B[Format Request] + B --> C[Add Signer UUID] + C --> D[Neynar API Call] + D --> E{Response} + E -->|Success| F[Store Cast Hash] + E -->|Error| G[Retry Logic] + G --> H[Exponential Backoff] + H --> D +``` + +## Interaction Patterns + +### Reply Chains + +```mermaid +graph TD + A[Original Cast] --> B[Agent Reply 1] + B --> C[User Reply] + C --> D[Agent Reply 2] + D --> E[Thread Continuation] + + style A fill:#f9f,stroke:#333,stroke-width:2px + style B fill:#9ff,stroke:#333,stroke-width:2px + style C fill:#ff9,stroke:#333,stroke-width:2px + style D fill:#9ff,stroke:#333,stroke-width:2px +``` + +### Channel Participation + +```mermaid +graph TD + A[Monitor Channel] --> B{New Cast} + B --> C[Evaluate Relevance] + C --> D{Relevant?} + D -->|Yes| E[Analyze Context] + D -->|No| F[Skip] + E --> G{Can Contribute?} + G -->|Yes| H[Post to Channel] + G -->|No| I[Like/Recast Only] +``` + +## Rate Limiting & Throttling + +```mermaid +graph TD + A[Action Request] --> B{Check Rate Limit} + B -->|Under Limit| C[Execute Action] + B -->|At Limit| D[Queue Action] + D --> E[Wait Period] + E --> F[Retry Queue] + F --> B + C --> G[Update Counter] + G --> H[Reset Timer] +``` + +## Error Handling Flow + +```mermaid +graph TD + A[Cast Attempt] --> B{Success?} + B -->|Yes| C[Complete] + B -->|No| D{Error Type} + D -->|Network| E[Retry with Backoff] + D -->|Validation| F[Fix & Retry] + D -->|Rate Limit| G[Queue for Later] + D -->|Fatal| H[Log & Abandon] + + E --> I{Max Retries?} + I -->|No| A + I -->|Yes| H + F --> A + G --> J[Delayed Retry] + J --> A +``` + +## Performance Metrics + +### Processing Times + +```mermaid +graph LR + A[Event Receipt] -->|~50ms| B[Classification] + B -->|~100ms| C[Analysis] + C -->|~200ms| D[Response Gen] + D -->|~50ms| E[Formatting] + E -->|~100ms| F[Submission] + F -->|~50ms| G[Confirmation] +``` + +### Throughput Management + +```mermaid +graph TD + A[Incoming Events] --> B[Event Queue] + B --> C{Queue Size} + C -->|Low| D[Process Immediately] + C -->|Medium| E[Batch Process] + C -->|High| F[Priority Filter] + F --> G[Process High Priority] + F --> H[Defer Low Priority] +``` + +## State Management + +```mermaid +graph TD + A[Plugin State] --> B[Active Conversations] + A --> C[Pending Responses] + A --> D[Rate Limit Status] + A --> E[Neynar API Status] + + B --> F[Thread Contexts] + C --> G[Response Queue] + D --> H[Cooldown Timers] + E --> I[API Health Check] +``` + +## Monitoring & Observability + +```mermaid +graph TD + A[Cast Activity] --> B[Metrics Collector] + B --> C[Response Times] + B --> D[Success Rates] + B --> E[Engagement Metrics] + B --> F[Error Rates] + + C --> G[Dashboard] + D --> G + E --> G + F --> G + + G --> H[Alerts] + H --> I[Auto-Scaling] + H --> J[Manual Intervention] +``` + +## Best Practices + +1. **Efficient Polling**: Use appropriate intervals to balance responsiveness and API rate limits +2. **Smart Caching**: Cache user profiles and recent casts to reduce Neynar API calls +3. **Graceful Degradation**: Handle API failures without losing queued responses +4. **Context Awareness**: Maintain conversation context across reply chains +5. **Rate Limit Respect**: Implement proper backoff strategies for Neynar API limits + +## Debugging Cast Flow + +Enable detailed logging to trace cast processing: + +```typescript +// Enable debug mode +process.env.FARCASTER_DEBUG = 'true'; + +// Log each stage +runtime.on('farcaster:event', (event) => { + console.log(`[${event.stage}]`, event.data); +}); +``` + +## Summary + +The Farcaster cast flow is designed to be: +- **Responsive**: Quick reaction to mentions and replies +- **Intelligent**: Context-aware response generation +- **Reliable**: Robust error handling and retry logic +- **Scalable**: Efficient queue management and rate limiting +- **Observable**: Comprehensive metrics and logging diff --git a/plugins/platform/farcaster/developer-guide.mdx b/plugins/platform/farcaster/developer-guide.mdx new file mode 100644 index 0000000..65f8600 --- /dev/null +++ b/plugins/platform/farcaster/developer-guide.mdx @@ -0,0 +1,561 @@ +--- +title: "Developer Guide" +description: "Comprehensive technical reference for the @elizaos/plugin-farcaster package" +--- + +# Farcaster Plugin Developer Guide + +## Overview + +The @elizaos/plugin-farcaster plugin enables elizaOS agents to interact with the Farcaster protocol through the Neynar API. This plugin provides comprehensive functionality for casting, replying, and engaging with the Farcaster ecosystem. + +## Core Features + +### 1. Casting Capabilities +- **Autonomous Casting**: Post original casts based on agent personality +- **Threaded Conversations**: Support for reply chains and threads +- **Media Support**: Embed images, links, and frames in casts +- **Scheduled Posting**: Time-based cast scheduling + +### 2. Engagement Features +- **Reply Detection**: Monitor and respond to mentions and replies +- **Like/Recast**: Programmatic engagement with other casts +- **Follow Management**: Automatic follow/unfollow based on criteria +- **Channel Support**: Post to specific channels (e.g., /elizaos) + +### 3. Hub Integration +- **Hub API**: Direct integration with Farcaster hubs +- **Message Validation**: Cryptographic message signing +- **Protocol Compliance**: Full Farcaster protocol v2 support + +## Installation + +```bash +# Using bun +bun add @elizaos/plugin-farcaster + +# Using npm +npm install @elizaos/plugin-farcaster + +# Using pnpm +pnpm add @elizaos/plugin-farcaster +``` + +## Configuration + +### Environment Variables + +```env +# Required +FARCASTER_NEYNAR_API_KEY=your-neynar-api-key +FARCASTER_SIGNER_UUID=your-signer-uuid +FARCASTER_FID=12345 + +# Feature Toggles +ENABLE_CAST=true +ENABLE_ACTION_PROCESSING=false +FARCASTER_DRY_RUN=false + +# Timing Configuration (in minutes) +CAST_INTERVAL_MIN=90 +CAST_INTERVAL_MAX=180 +FARCASTER_POLL_INTERVAL=2 +ACTION_INTERVAL=5 + +# Other Options +CAST_IMMEDIATELY=false +ACTION_TIMELINE_TYPE=ForYou +MAX_ACTIONS_PROCESSING=1 +MAX_CAST_LENGTH=320 +``` + +### Character Configuration + +```typescript +import { Character } from "@elizaos/core"; +import { farcasterPlugin } from "@elizaos/plugin-farcaster"; + +export const character: Character = { + name: "FarcasterAgent", + plugins: [farcasterPlugin], + settings: { + farcaster: { + channels: ["/elizaos", "/ai16z"], + replyProbability: 0.7, + castStyle: "conversational", + maxCastLength: 320 + } + } +}; +``` + +## Actions + +### SEND_CAST + +Posts a new cast to Farcaster. + +```typescript +{ + name: "SEND_CAST", + description: "Posts a cast (message) on Farcaster", + examples: [ + "Can you post about the new ElizaOS features on Farcaster?", + "Share on Farcaster that we just launched version 2.0!" + ] +} +``` + +### REPLY_TO_CAST + +Reply to an existing cast. + +```typescript +{ + name: "REPLY_TO_CAST", + description: "Replies to a cast on Farcaster", + examples: [ + "Someone asked about ElizaOS on Farcaster, can you reply?", + "Reply to that cast and thank them for the feedback" + ] +} +``` + +## Providers + +### farcasterProfile + +Provides the agent's Farcaster profile information. + +```typescript +// Provider name: 'farcasterProfile' +const profile = await runtime.providers.farcasterProfile.get(runtime, message, state); +// Returns profile data including FID, username, bio, etc. +``` + +### farcasterTimeline + +Supplies recent timeline casts for context. + +```typescript +// Provider name: 'farcasterTimeline' +const timeline = await runtime.providers.farcasterTimeline.get(runtime, message, state); +// Returns recent casts from the agent's timeline +``` + +## Events + +### handleCastSent + +Triggered when a cast is successfully sent. Stores metadata for tracking: + +```typescript +// Automatically handled when casting +// Stores cast hash, thread ID, and message metadata +EventType: 'cast:sent' +Payload: { + castHash: string, + threadId: string, + messageId: UUID, + platform: 'farcaster' +} +``` + +### handleMessageReceived + +Processes incoming Farcaster messages and creates memories: + +```typescript +// Automatically triggered for incoming messages +EventType: 'message:received' +Payload: { + cast: Cast, + profile: Profile, + threadId: string +} +``` + +## Managers + +### FarcasterAgentManager + +Orchestrates all Farcaster operations for an agent: + +```typescript +class FarcasterAgentManager { + client: FarcasterClient // Neynar API client + casts: FarcasterCastManager // Autonomous posting + interactions: FarcasterInteractionManager // Mentions/replies + + async start() // Start all managers + async stop() // Stop all managers +} +``` + +### FarcasterCastManager + +Handles autonomous casting based on configuration: + +```typescript +class FarcasterCastManager { + // Manages periodic autonomous posts + // Respects CAST_INTERVAL_MIN/MAX settings + // Handles CAST_IMMEDIATELY flag + + async start() // Begin autonomous casting + async stop() // Stop casting + async publishCast(text: string) // Manually publish +} +``` + +### FarcasterInteractionManager + +Processes mentions, replies, and interactions: + +```typescript +class FarcasterInteractionManager { + // Polls for mentions at FARCASTER_POLL_INTERVAL + // Processes up to MAX_ACTIONS_PROCESSING per cycle + // Uses AI to determine appropriate responses + + async start() // Start monitoring + async stop() // Stop monitoring + async processInteractions() // Process pending interactions +} +``` + +## Services + +### FarcasterService + +Main service coordinating all Farcaster operations: + +```typescript +class FarcasterService extends Service { + static serviceType = 'farcaster' + + // Service lifecycle + async initialize(runtime: IAgentRuntime): Promise + static async start(runtime: IAgentRuntime): Promise + static async stop(runtime: IAgentRuntime): Promise + + // Get service instances + getMessageService(agentId: UUID): FarcasterMessageService + getCastService(agentId: UUID): FarcasterCastService + getActiveManagers(): Map + + // Health check + async healthCheck(): Promise +} +``` + +### MessageService + +Implements IMessageService for message operations: + +```typescript +class FarcasterMessageService implements IMessageService { + // Message retrieval + async getMessages(options: GetMessagesOptions): Promise + async getMessage(messageId: string): Promise + + // Message sending + async sendMessage(options: { + text: string, + type: FarcasterMessageType, + replyToId?: string + }): Promise +} +``` + +### CastService + +Implements IPostService with full CRUD operations: + +```typescript +class FarcasterCastService implements IPostService { + // Cast operations + async getCasts(params: { + agentId: UUID, + limit?: number, + cursor?: string + }): Promise + + async createCast(params: { + text: string, + media?: string[], + replyTo?: { hash: string, fid: number } + }): Promise + + async deleteCast(castHash: string): Promise + + // Engagement operations + async likeCast(castHash: string): Promise + async unlikeCast(castHash: string): Promise + async recast(castHash: string): Promise + async unrecast(castHash: string): Promise + + // Utility methods + async publishCast(text: string): Promise + async getCastByHash(hash: string): Promise + async getProfile(fid: number): Promise +} +``` + +## Client Architecture + +### FarcasterClient + +Core client wrapping Neynar API operations: + +```typescript +class FarcasterClient { + private neynar: NeynarAPIClient; + private signerUuid: string; + + constructor(params: { + neynar: NeynarAPIClient, + signerUuid: string + }) + + // Casting operations + async publishCast(text: string, options?: { + embeds?: Array<{ url: string }>, + replyTo?: string, + channelId?: string + }): Promise + + async reply(params: { + text: string, + replyTo: { hash: string, fid: number } + }): Promise + + async deleteCast(targetHash: string): Promise + + // User operations + async getUser(): Promise + async getUserByFid(fid: number): Promise + async getUserByUsername(username: string): Promise + + // Timeline operations + async getMentions(fid: number, cursor?: string): Promise + async getTimeline(type: 'ForYou' | 'Following', cursor?: string): Promise + async getCast(hash: string): Promise + + // Engagement operations + async likeCast(targetHash: string): Promise + async unlikeCast(targetHash: string): Promise + async recast(targetHash: string): Promise + async unrecast(targetHash: string): Promise + async followUser(targetFid: number): Promise + async unfollowUser(targetFid: number): Promise +} +``` + +### Common Utilities + +#### AsyncQueue +Manages asynchronous operations with concurrency control: + +```typescript +class AsyncQueue { + constructor(concurrency: number) + push(fn: () => Promise): Promise +} +``` + +#### Helper Functions + +```typescript +// Cast utilities +castUuid(cast: Cast): UUID // Generate unique ID for cast +neynarCastToCast(cast: NeynarCast): Cast // Convert Neynar format +formatCastTimestamp(timestamp: number): string // Format timestamps + +// Prompt formatting +formatCast(cast: Cast): string // Format cast for AI processing +formatTimeline(casts: Cast[]): string // Format timeline for context + +// Cache management +lastCastCacheKey(agentId: UUID): string // Generate cache keys +``` + +## Event System + +### Cast Events + +```typescript +runtime.on("cast:new", (cast: Cast) => { + // Handle new cast +}); + +runtime.on("cast:reply", (reply: CastReply) => { + // Handle reply +}); + +runtime.on("cast:like", (like: CastLike) => { + // Handle like +}); +``` + +### Error Events + +```typescript +runtime.on("farcaster:error", (error: FarcasterError) => { + // Handle error +}); +``` + +## Memory & Storage + +### Memory System + +The plugin uses elizaOS's memory system for persistence rather than direct database tables: + +```typescript +// Cast metadata stored when sending +await runtime.createMemory({ + type: 'metadata', + content: { + castHash: string, + threadId: string, + platform: 'farcaster', + messageId: UUID, + sentAt: number + } +}); + +// Message memory for each cast +await runtime.createMemory({ + type: 'message', + content: { + text: string, + source: 'farcaster', + hash: string, + fid: number, + timestamp: number, + inReplyTo?: string + } +}); +``` + +### Caching Strategy + +LRU cache for performance optimization: +- **Cast Cache**: TTL 30 minutes, 9000 entries max +- **Profile Cache**: User profile data +- **Timeline Cache**: Recent timeline casts +- **Last Cast Tracking**: Per-agent last cast timestamps + +## Security Considerations + +### Key Management +- Store API keys and signer UUIDs securely using environment variables +- Never commit credentials to version control +- Use separate Neynar API keys for development and production +- Create separate signers for different environments + +### Rate Limiting +- Implement exponential backoff for API requests +- Respect hub rate limits (typically 100 req/min) +- Cache frequently accessed data + +### Content Validation +- Validate cast length (max 320 characters) +- Sanitize user inputs +- Verify message signatures + +## Performance Optimization + +### AsyncQueue Implementation +The plugin uses an async queue to prevent rate limiting: +```typescript +// Queue processes operations with concurrency control +const asyncQueue = new AsyncQueue(1); // Single concurrency +await asyncQueue.push(() => processInteraction(cast)); +``` + +### Polling Optimization +```typescript +// Configurable polling intervals to balance responsiveness +FARCASTER_POLL_INTERVAL=2 // Minutes between polls +ACTION_INTERVAL=5 // Minutes between action processing +MAX_ACTIONS_PROCESSING=1 // Actions per cycle +``` + +## Troubleshooting + +### Common Issues + +1. **Authentication Errors** + - Verify mnemonic is correct + - Ensure FID matches the mnemonic + - Check hub connectivity + +2. **Rate Limiting** + - Implement retry logic with backoff + - Use caching to reduce API calls + - Monitor rate limit headers + +3. **Message Validation Failures** + - Verify timestamp is within valid range + - Ensure proper message formatting + - Check signature validity + +### Debug Mode + +Enable debug logging: + +```env +FARCASTER_DEBUG=true +LOG_LEVEL=debug +``` + +## Best Practices + +1. **Content Strategy** + - Keep casts concise and engaging + - Use channels appropriately + - Maintain consistent voice + +2. **Engagement Guidelines** + - Don't spam or over-engage + - Respect community norms + - Build genuine connections + +3. **Technical Implementation** + - Handle errors gracefully + - Implement proper retry logic + - Monitor performance metrics + +## Migration Guide + +### From v1 to v2 + +```typescript +// v1 +import { FarcasterPlugin } from "@elizaos/plugin-farcaster"; + +// v2 +import { farcasterPlugin } from "@elizaos/plugin-farcaster"; + +// Configuration changes +// v1: Plugin initialized with options +const plugin = new FarcasterPlugin(options); + +// v2: Configuration via environment and character +const character = { + plugins: [farcasterPlugin], + settings: { farcaster: options } +}; +``` + +## Support + +- **GitHub**: [elizaos-plugins/plugin-farcaster](https://github.com/elizaos-plugins/plugin-farcaster) +- **Discord**: Join the elizaOS community +- **Documentation**: [elizaos.ai/docs](https://elizaos.ai/docs) + +## License + +MIT License - see LICENSE file for details diff --git a/plugins/platform/farcaster/examples.mdx b/plugins/platform/farcaster/examples.mdx new file mode 100644 index 0000000..35867bc --- /dev/null +++ b/plugins/platform/farcaster/examples.mdx @@ -0,0 +1,525 @@ +--- +title: "Examples" +description: "Practical implementation examples for the @elizaos/plugin-farcaster package" +--- + +# Farcaster Plugin Examples + +## Basic Setup + +### Minimal Configuration + +```typescript +// character.ts +import { Character } from "@elizaos/core"; +import { farcasterPlugin } from "@elizaos/plugin-farcaster"; + +export const character: Character = { + name: "MyFarcasterAgent", + plugins: [farcasterPlugin], + bio: "An AI agent exploring the Farcaster ecosystem", + description: "I engage thoughtfully with the Farcaster community" +}; +``` + +### Environment Configuration + +```env +# .env file +FARCASTER_NEYNAR_API_KEY=your-neynar-api-key +FARCASTER_SIGNER_UUID=your-signer-uuid +FARCASTER_FID=12345 +ENABLE_CAST=true +ENABLE_ACTION_PROCESSING=false +FARCASTER_DRY_RUN=false +``` + +## Casting Examples + +### Simple Cast + +```typescript +// Post a simple cast +import { runtime } from "@elizaos/core"; + +async function postSimpleCast() { + const action = runtime.getAction("SEND_CAST"); + + await action.handler(runtime, { + text: "Hello Farcaster! Excited to be here 🎉" + }); +} +``` + +### Cast with Channel + +```typescript +// Post to a specific channel +async function postToChannel() { + const action = runtime.getAction("SEND_CAST"); + + await action.handler(runtime, { + text: "Building with elizaOS is amazing!", + channel: "/elizaos" + }); +} +``` + +### Cast with Embeds + +```typescript +// Post with embedded content +async function postWithEmbed() { + const action = runtime.getAction("SEND_CAST"); + + await action.handler(runtime, { + text: "Check out this awesome project!", + embeds: [ + { url: "https://github.com/elizaos/elizaos" } + ] + }); +} +``` + +### Thread Creation + +```typescript +// Create a thread of casts +async function createThread() { + const action = runtime.getAction("SEND_CAST"); + + // First cast + const firstCast = await action.handler(runtime, { + text: "Let me explain how elizaOS agents work 🧵" + }); + + // Reply to create thread + const replyAction = runtime.getAction("REPLY_TO_CAST"); + + await replyAction.handler(runtime, { + text: "1/ Agents are autonomous entities that can interact across platforms", + targetCastHash: firstCast.hash, + targetFid: firstCast.fid + }); + + await replyAction.handler(runtime, { + text: "2/ They use LLMs for natural language understanding and generation", + targetCastHash: firstCast.hash, + targetFid: firstCast.fid + }); +} +``` + +## Reply Examples + +### Simple Reply + +```typescript +// Reply to a cast +async function replyToCast(castHash: string, authorFid: number) { + const action = runtime.getAction("REPLY_TO_CAST"); + + await action.handler(runtime, { + text: "Great point! I completely agree with this perspective.", + targetCastHash: castHash, + targetFid: authorFid + }); +} +``` + +### Contextual Reply + +```typescript +// Reply with context awareness +async function contextualReply(cast: Cast) { + const context = await buildContext(cast); + const response = await generateResponse(context); + + const action = runtime.getAction("REPLY_TO_CAST"); + + await action.handler(runtime, { + text: response, + targetCastHash: cast.hash, + targetFid: cast.author.fid + }); +} + +async function buildContext(cast: Cast) { + // Get thread history + const thread = await getThreadHistory(cast); + + // Get author profile + const author = await getProfile(cast.author.fid); + + return { + originalCast: cast, + thread: thread, + author: author, + topics: extractTopics(cast.text) + }; +} +``` + +## Engagement Examples + +### Engagement Note + +```typescript +// Note: Like, recast, and follow functionality are managed internally +// by the FarcasterService and MessageService based on agent behavior +// and are not exposed as direct actions at this time. +``` + +## Service Integration Examples + +### Custom Service Implementation + +```typescript +import { Service, IAgentRuntime } from "@elizaos/core"; +import { NeynarAPIClient } from "@neynar/nodejs-sdk"; + +class CustomFarcasterService implements Service { + private client: NeynarAPIClient; + private runtime: IAgentRuntime; + + async start(runtime: IAgentRuntime): Promise { + this.runtime = runtime; + this.client = new NeynarAPIClient({ + apiKey: process.env.FARCASTER_NEYNAR_API_KEY! + }); + + // Start monitoring + this.startMonitoring(); + } + + private async startMonitoring() { + // Monitor mentions + setInterval(async () => { + const mentions = await this.client.getMentions(); + + for (const mention of mentions) { + await this.handleMention(mention); + } + }, 30000); // Check every 30 seconds + } + + private async handleMention(mention: Cast) { + // Generate response + const response = await this.generateResponse(mention); + + // Reply + await this.client.reply(mention.hash, mention.author.fid, response); + } + + async stop(): Promise { + // Cleanup + await this.client.disconnect(); + } +} +``` + +### Event-Driven Responses + +```typescript +// Set up event listeners for Farcaster events +runtime.on("farcaster:mention", async (event) => { + const { cast, author } = event; + + // Check if we should respond + if (shouldRespond(cast)) { + const response = await generateResponse(cast); + + await replyToCast(cast.hash, author.fid, response); + } +}); + +runtime.on("farcaster:followed", async (event) => { + const { follower } = event; + + // Auto-follow back + await followUser(follower.fid); + + // Send welcome message + await postCast(`Welcome @${follower.username}! Looking forward to our interactions.`); +}); +``` + +## Advanced Patterns + +### Scheduled Casting + +```typescript +// Schedule regular casts +class ScheduledCaster { + private runtime: IAgentRuntime; + + constructor(runtime: IAgentRuntime) { + this.runtime = runtime; + } + + start() { + // Morning update + this.scheduleDaily("09:00", async () => { + await this.postMorningUpdate(); + }); + + // Evening reflection + this.scheduleDaily("21:00", async () => { + await this.postEveningReflection(); + }); + } + + private async postMorningUpdate() { + const insights = await this.generateDailyInsights(); + + await postCast({ + text: `Good morning! Today's insight: ${insights}`, + channel: "/elizaos" + }); + } + + private async postEveningReflection() { + const reflection = await this.generateReflection(); + + await postCast({ + text: `Evening thoughts: ${reflection}`, + channel: "/elizaos" + }); + } +} +``` + +### Channel-Specific Behavior + +```typescript +// Different behavior for different channels +class ChannelManager { + private channelConfigs = { + "/elizaos": { + style: "technical", + replyProbability: 0.8, + topics: ["AI", "agents", "development"] + }, + "/ai16z": { + style: "philosophical", + replyProbability: 0.6, + topics: ["AI", "future", "technology"] + }, + "/base": { + style: "friendly", + replyProbability: 0.5, + topics: ["community", "building", "web3"] + } + }; + + async handleChannelCast(cast: Cast, channel: string) { + const config = this.channelConfigs[channel]; + + if (!config) return; + + // Check if topic matches + const relevantTopic = config.topics.some(topic => + cast.text.toLowerCase().includes(topic) + ); + + if (relevantTopic && Math.random() < config.replyProbability) { + const response = await this.generateResponse(cast, config.style); + await this.reply(cast, response); + } + } +} +``` + +### Conversation Memory + +```typescript +// Track conversation history +class ConversationTracker { + private conversations = new Map(); + + async handleCast(cast: Cast) { + const threadId = cast.threadHash || cast.hash; + + // Get or create conversation + let conversation = this.conversations.get(threadId); + + if (!conversation) { + conversation = { + id: threadId, + participants: new Set([cast.author.fid]), + messages: [], + startTime: Date.now() + }; + this.conversations.set(threadId, conversation); + } + + // Add message to conversation + conversation.messages.push({ + author: cast.author.fid, + text: cast.text, + timestamp: cast.timestamp + }); + + // Generate contextual response + const response = await this.generateContextualResponse(conversation); + + if (response) { + await this.reply(cast, response); + } + } +} +``` + +### Multi-Platform Coordination + +```typescript +// Coordinate between Farcaster and other platforms +class MultiPlatformAgent { + async crossPost(content: string) { + // Post to Farcaster + await this.postToFarcaster(content); + + // Post to Twitter + if (this.runtime.hasPlugin("twitter")) { + await this.postToTwitter(content); + } + + // Post to Discord + if (this.runtime.hasPlugin("discord")) { + await this.postToDiscord(content); + } + } + + async syncEngagement() { + // Get Farcaster engagement + const farcasterLikes = await this.getFarcasterLikes(); + + // Mirror high-engagement content to other platforms + for (const cast of farcasterLikes) { + if (cast.reactions.count > 10) { + await this.crossPost(cast.text); + } + } + } +} +``` + +## Error Handling Examples + +### Robust Cast Posting + +```typescript +async function robustCastPost(text: string, maxRetries = 3) { + let attempt = 0; + let lastError; + + while (attempt < maxRetries) { + try { + const result = await postCast({ text }); + return result; + } catch (error) { + lastError = error; + attempt++; + + if (error.code === 'RATE_LIMIT') { + // Wait with exponential backoff + await wait(Math.pow(2, attempt) * 1000); + } else if (error.code === 'NETWORK_ERROR') { + // Retry immediately for network errors + continue; + } else { + // Unknown error, throw immediately + throw error; + } + } + } + + throw new Error(`Failed after ${maxRetries} attempts: ${lastError}`); +} +``` + +### Validation and Sanitization + +```typescript +function validateCast(text: string): boolean { + // Check length + if (text.length > 320) { + throw new Error("Cast exceeds maximum length of 320 characters"); + } + + // Check for required content + if (text.trim().length === 0) { + throw new Error("Cast cannot be empty"); + } + + // Check for spam patterns + if (isSpam(text)) { + throw new Error("Cast appears to be spam"); + } + + return true; +} + +function sanitizeCast(text: string): string { + // Remove excessive whitespace + text = text.replace(/\s+/g, ' ').trim(); + + // Remove invalid characters + text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); + + // Truncate if needed + if (text.length > 320) { + text = text.substring(0, 317) + "..."; + } + + return text; +} +``` + +## Testing Examples + +```typescript +// Mock testing setup +import { describe, it, expect, beforeEach } from "bun:test"; +import { MockFarcasterClient } from "@elizaos/plugin-farcaster/test"; + +describe("Farcaster Plugin", () => { + let client: MockFarcasterClient; + + beforeEach(() => { + client = new MockFarcasterClient(); + }); + + it("should post a cast", async () => { + const result = await client.postCast("Test cast"); + + expect(result.hash).toBeDefined(); + expect(result.text).toBe("Test cast"); + }); + + it("should handle replies", async () => { + const original = await client.postCast("Original"); + const reply = await client.reply( + original.hash, + original.fid, + "Reply text" + ); + + expect(reply.parentHash).toBe(original.hash); + }); +}); +``` + +## Summary + +These examples demonstrate the flexibility and power of the Farcaster plugin. Key patterns include: +- Simple and complex casting scenarios using SEND_CAST +- Intelligent reply systems using REPLY_TO_CAST +- Channel-specific behaviors +- Cross-platform coordination +- Robust error handling +- Testing strategies + +The plugin uses the Neynar API for all Farcaster interactions, requiring proper API key and signer configuration. + +For more advanced use cases, combine these patterns with the elizaOS agent framework's other capabilities. diff --git a/plugins/platform/farcaster/testing-guide.mdx b/plugins/platform/farcaster/testing-guide.mdx new file mode 100644 index 0000000..5685819 --- /dev/null +++ b/plugins/platform/farcaster/testing-guide.mdx @@ -0,0 +1,629 @@ +--- +title: "Testing Guide" +description: "Comprehensive testing strategies and patterns for the @elizaos/plugin-farcaster package" +--- + +# Farcaster Plugin Testing Guide + +## Overview + +This guide provides comprehensive testing strategies for the Farcaster plugin, covering unit tests, integration tests, and end-to-end testing scenarios. + +## Test Environment Setup + +### Configuration + +```typescript +// test/setup.ts +import { beforeAll, afterAll } from "bun:test"; +import { TestEnvironment } from "@elizaos/test-utils"; + +let testEnv: TestEnvironment; + +beforeAll(async () => { + testEnv = new TestEnvironment({ + plugins: ["@elizaos/plugin-farcaster"], + mockServices: true + }); + + // Set test environment variables + process.env.FARCASTER_DRY_RUN = "true"; + process.env.FARCASTER_HUB_URL = "http://localhost:8080"; + process.env.NODE_ENV = "test"; + + await testEnv.start(); +}); + +afterAll(async () => { + await testEnv.cleanup(); +}); +``` + +### Mock Hub Server + +```typescript +// test/mocks/hub-server.ts +import { createServer } from "http"; + +export class MockHubServer { + private server: any; + private responses: Map = new Map(); + + async start(port = 8080) { + this.server = createServer((req, res) => { + const response = this.responses.get(req.url!) || { + error: "Not found" + }; + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(response)); + }); + + await new Promise(resolve => { + this.server.listen(port, resolve); + }); + } + + setResponse(path: string, response: any) { + this.responses.set(path, response); + } + + async stop() { + await new Promise(resolve => this.server.close(resolve)); + } +} +``` + +## Unit Tests + +### Action Tests + +```typescript +// test/actions/post-cast.test.ts +import { describe, it, expect, beforeEach } from "bun:test"; +import { postCastAction } from "@elizaos/plugin-farcaster"; +import { createMockRuntime } from "@elizaos/test-utils"; + +describe("POST_CAST Action", () => { + let runtime: any; + + beforeEach(() => { + runtime = createMockRuntime(); + }); + + it("should validate cast text length", async () => { + const longText = "a".repeat(321); + + await expect( + postCastAction.handler(runtime, { text: longText }) + ).rejects.toThrow("Cast exceeds maximum length"); + }); + + it("should post a simple cast", async () => { + const result = await postCastAction.handler(runtime, { + text: "Test cast" + }); + + expect(result.success).toBe(true); + expect(result.cast).toBeDefined(); + expect(result.cast.text).toBe("Test cast"); + }); + + it("should handle channel posts", async () => { + const result = await postCastAction.handler(runtime, { + text: "Channel test", + channel: "/elizaos" + }); + + expect(result.cast.channel).toBe("/elizaos"); + }); + + it("should support embeds", async () => { + const result = await postCastAction.handler(runtime, { + text: "Cast with embed", + embeds: [{ url: "https://example.com" }] + }); + + expect(result.cast.embeds).toHaveLength(1); + expect(result.cast.embeds[0].url).toBe("https://example.com"); + }); +}); +``` + +### Provider Tests + +```typescript +// test/providers/cast-provider.test.ts +import { describe, it, expect } from "bun:test"; +import { castProvider } from "@elizaos/plugin-farcaster"; +import { createMockRuntime } from "@elizaos/test-utils"; + +describe("Cast Provider", () => { + it("should fetch recent casts", async () => { + const runtime = createMockRuntime(); + const casts = await castProvider.getCasts(runtime, { + limit: 10 + }); + + expect(Array.isArray(casts)).toBe(true); + expect(casts.length).toBeLessThanOrEqual(10); + }); + + it("should filter by channel", async () => { + const runtime = createMockRuntime(); + const casts = await castProvider.getCasts(runtime, { + channel: "/elizaos", + limit: 5 + }); + + casts.forEach(cast => { + expect(cast.channel).toBe("/elizaos"); + }); + }); + + it("should include replies when requested", async () => { + const runtime = createMockRuntime(); + const casts = await castProvider.getCasts(runtime, { + includeReplies: true + }); + + const replies = casts.filter(c => c.parentHash); + expect(replies.length).toBeGreaterThan(0); + }); +}); +``` + +### Evaluator Tests + +```typescript +// test/evaluators/engagement.test.ts +import { describe, it, expect } from "bun:test"; +import { engagementEvaluator } from "@elizaos/plugin-farcaster"; + +describe("Engagement Evaluator", () => { + it("should evaluate high-quality casts positively", async () => { + const cast = { + text: "Just deployed a new feature for elizaOS agents!", + author: { fid: 123, username: "dev" }, + reactions: { count: 15 }, + recasts: { count: 5 } + }; + + const score = await engagementEvaluator.evaluate(cast); + expect(score).toBeGreaterThan(0.7); + }); + + it("should evaluate spam negatively", async () => { + const cast = { + text: "Buy now! Click here! Limited offer!", + author: { fid: 456, username: "spammer" }, + reactions: { count: 0 }, + recasts: { count: 0 } + }; + + const score = await engagementEvaluator.evaluate(cast); + expect(score).toBeLessThan(0.3); + }); + + it("should consider author reputation", async () => { + const cast = { + text: "Interesting thought", + author: { + fid: 789, + username: "trusted", + followerCount: 1000 + } + }; + + const score = await engagementEvaluator.evaluate(cast); + expect(score).toBeGreaterThan(0.5); + }); +}); +``` + +## Integration Tests + +### Service Integration + +```typescript +// test/integration/service.test.ts +import { describe, it, expect, beforeAll, afterAll } from "bun:test"; +import { FarcasterService } from "@elizaos/plugin-farcaster"; +import { createTestRuntime } from "@elizaos/test-utils"; +import { MockHubServer } from "../mocks/hub-server"; + +describe("Farcaster Service Integration", () => { + let service: FarcasterService; + let runtime: any; + let hubServer: MockHubServer; + + beforeAll(async () => { + hubServer = new MockHubServer(); + await hubServer.start(); + + runtime = await createTestRuntime(); + service = new FarcasterService(); + await service.start(runtime); + }); + + afterAll(async () => { + await service.stop(); + await hubServer.stop(); + }); + + it("should connect to hub", async () => { + expect(service.isConnected()).toBe(true); + }); + + it("should post and retrieve casts", async () => { + const cast = await service.postCast("Integration test"); + expect(cast.hash).toBeDefined(); + + const retrieved = await service.getCast(cast.hash); + expect(retrieved.text).toBe("Integration test"); + }); + + it("should handle reply chains", async () => { + const original = await service.postCast("Original cast"); + const reply = await service.replyCast( + "Reply to original", + original.hash, + original.fid + ); + + expect(reply.parentHash).toBe(original.hash); + + const thread = await service.getThread(original.hash); + expect(thread).toHaveLength(2); + }); +}); +``` + +### Event System Tests + +```typescript +// test/integration/events.test.ts +import { describe, it, expect } from "bun:test"; +import { createTestRuntime } from "@elizaos/test-utils"; +import { farcasterPlugin } from "@elizaos/plugin-farcaster"; + +describe("Farcaster Event System", () => { + it("should emit cast events", async () => { + const runtime = await createTestRuntime({ + plugins: [farcasterPlugin] + }); + + let eventFired = false; + runtime.on("farcaster:cast:new", () => { + eventFired = true; + }); + + await runtime.action("POST_CAST", { + text: "Event test" + }); + + await new Promise(resolve => setTimeout(resolve, 100)); + expect(eventFired).toBe(true); + }); + + it("should handle mention events", async () => { + const runtime = await createTestRuntime({ + plugins: [farcasterPlugin] + }); + + const mentions: any[] = []; + runtime.on("farcaster:mention", (event) => { + mentions.push(event); + }); + + // Simulate incoming mention + await runtime.simulateEvent("farcaster:mention", { + cast: { + text: "@agent hello!", + author: { fid: 123 } + } + }); + + expect(mentions).toHaveLength(1); + expect(mentions[0].cast.text).toContain("@agent"); + }); +}); +``` + +## End-to-End Tests + +### Full Flow Test + +```typescript +// test/e2e/full-flow.test.ts +import { describe, it, expect } from "bun:test"; +import { createAgent } from "@elizaos/core"; +import { farcasterPlugin } from "@elizaos/plugin-farcaster"; + +describe("E2E: Farcaster Agent Flow", () => { + it("should perform complete interaction flow", async () => { + // Create agent with Farcaster plugin + const agent = await createAgent({ + name: "TestAgent", + plugins: [farcasterPlugin], + env: { + FARCASTER_MNEMONIC: "test mnemonic ...", + FARCASTER_FID: "99999", + FARCASTER_DRY_RUN: "true" + } + }); + + // Start agent + await agent.start(); + + // Post initial cast + const cast = await agent.execute("POST_CAST", { + text: "Hello from test agent!" + }); + + expect(cast.success).toBe(true); + + // Simulate incoming reply + await agent.handleEvent({ + type: "farcaster:reply", + data: { + cast: { + text: "Welcome to Farcaster!", + parentHash: cast.hash, + author: { fid: 123 } + } + } + }); + + // Check if agent responded + const responses = await agent.getResponses(); + expect(responses).toHaveLength(1); + expect(responses[0].type).toBe("REPLY_CAST"); + + // Stop agent + await agent.stop(); + }); +}); +``` + +### Load Testing + +```typescript +// test/load/cast-load.test.ts +import { describe, it, expect } from "bun:test"; +import { FarcasterService } from "@elizaos/plugin-farcaster"; +import { createTestRuntime } from "@elizaos/test-utils"; + +describe("Load Testing", () => { + it("should handle rapid casting via actions", async () => { + const runtime = await createTestRuntime(); + const service = new FarcasterService(); + await service.start(runtime); + + const castService = service.getCastService(runtime.agentId); + const promises = []; + + // Send 50 casts rapidly + for (let i = 0; i < 50; i++) { + promises.push( + castService.publishCast(`Load test cast ${i}`) + .catch(err => ({ error: err })) + ); + } + + const results = await Promise.all(promises); + + // Check success rate + const successful = results.filter(r => !r.error); + const successRate = successful.length / results.length; + + expect(successRate).toBeGreaterThan(0.8); // 80% success rate + }); + + it("should handle concurrent message operations", async () => { + const runtime = await createTestRuntime(); + const service = new FarcasterService(); + await service.start(runtime); + + const messageService = service.getMessageService(runtime.agentId); + + // Perform multiple operations concurrently + const operations = await Promise.all([ + messageService.sendMessage({ text: "Concurrent 1" }), + messageService.sendMessage({ text: "Concurrent 2" }), + messageService.sendMessage({ text: "Concurrent 3" }) + ]); + + expect(operations).toHaveLength(3); + operations.forEach(op => { + expect(op.error).toBeUndefined(); + }); + }); +}); +``` + +## Mock Data Generators + +```typescript +// test/utils/generators.ts +export function generateMockCast(overrides = {}) { + return { + hash: `0x${Math.random().toString(16).slice(2)}`, + fid: Math.floor(Math.random() * 10000), + text: "Mock cast text", + timestamp: Date.now(), + author: { + fid: Math.floor(Math.random() * 10000), + username: `user${Math.floor(Math.random() * 1000)}`, + displayName: "Mock User", + pfp: "https://example.com/pfp.jpg" + }, + reactions: { + count: Math.floor(Math.random() * 100) + }, + recasts: { + count: Math.floor(Math.random() * 20) + }, + replies: { + count: Math.floor(Math.random() * 50) + }, + ...overrides + }; +} + +export function generateMockThread(depth = 3) { + const thread = []; + let parentHash = null; + + for (let i = 0; i < depth; i++) { + const cast = generateMockCast({ + text: `Thread message ${i + 1}`, + parentHash: parentHash + }); + thread.push(cast); + parentHash = cast.hash; + } + + return thread; +} +``` + +## Test Coverage + +### Coverage Configuration + +```json +// package.json +{ + "scripts": { + "test": "bun test", + "test:coverage": "bun test --coverage", + "test:watch": "bun test --watch" + } +} +``` + +### Coverage Report Example + +```bash +# Run tests with coverage +bun test --coverage + +# Output +-------------------|---------|----------|---------|---------| +File | % Stmts | % Branch | % Funcs | % Lines | +-------------------|---------|----------|---------|---------| +All files | 89.5 | 82.3 | 91.2 | 88.7 | + actions/ | 92.1 | 85.6 | 94.3 | 91.8 | + sendCast.ts | 93.5 | 87.2 | 95.0 | 93.1 | + replyCast.ts | 91.2 | 84.5 | 93.8 | 90.9 | + providers/ | 87.3 | 79.8 | 88.5 | 86.4 | + profileProvider | 88.1 | 81.2 | 89.3 | 87.5 | + timelineProvider | 87.0 | 80.1 | 88.0 | 86.2 | + services/ | 88.9 | 81.4 | 90.7 | 87.9 | + MessageService | 89.2 | 82.1 | 91.0 | 88.3 | + CastService | 88.5 | 80.7 | 90.4 | 87.5 | +-------------------|---------|----------|---------|---------| +``` + +## Debugging Tests + +### Debug Configuration + +```typescript +// test/debug.ts +export function enableDebugMode() { + process.env.DEBUG = "farcaster:*"; + process.env.LOG_LEVEL = "debug"; + process.env.FARCASTER_DEBUG = "true"; +} + +export function logTestContext(test: string, data: any) { + console.log(`[TEST: ${test}]`, JSON.stringify(data, null, 2)); +} +``` + +### Visual Test Output + +```typescript +// test/utils/visual.ts +export function visualizeCastThread(thread: Cast[]) { + console.log("\n📝 Cast Thread Visualization:"); + thread.forEach((cast, index) => { + const indent = " ".repeat(index); + console.log(`${indent}└─ ${cast.author.username}: ${cast.text}`); + }); + console.log("\n"); +} + +export function visualizeEngagement(cast: Cast) { + console.log("\n📊 Engagement Metrics:"); + console.log(` ❤️ Likes: ${cast.reactions.count}`); + console.log(` 🔄 Recasts: ${cast.recasts.count}`); + console.log(` 💬 Replies: ${cast.replies.count}`); + console.log("\n"); +} +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +# .github/workflows/test-farcaster.yml +name: Farcaster Plugin Tests + +on: + push: + paths: + - 'packages/plugin-farcaster/**' + pull_request: + paths: + - 'packages/plugin-farcaster/**' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test packages/plugin-farcaster + env: + FARCASTER_DRY_RUN: true + + - name: Generate coverage + run: bun test --coverage packages/plugin-farcaster + + - name: Upload coverage + uses: codecov/codecov-action@v3 +``` + +## Best Practices + +1. **Test Isolation**: Each test should be independent +2. **Mock External Services**: Never hit real Farcaster APIs in tests +3. **Use Test Fixtures**: Maintain consistent test data +4. **Test Edge Cases**: Include error scenarios and boundary conditions +5. **Performance Testing**: Include load and stress tests +6. **Documentation**: Keep tests as living documentation + +## Summary + +This testing guide provides comprehensive strategies for testing the Farcaster plugin: +- Unit tests for individual components +- Integration tests for service interactions +- End-to-end tests for complete flows +- Load testing for performance validation +- Mock utilities for consistent testing +- CI/CD integration for automated testing + +Following these patterns ensures robust and reliable Farcaster integration.