diff --git a/.env.example b/.env.example index 52fe7bb529..8203d10d2e 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ PGLITE_DATA_DIR= #../pgLite/ if selecting a directory --- or memory:// if # Eliza Port Config SERVER_PORT=3000 +VITE_SERVER_PORT=${SERVER_PORT} # Supabase Configuration SUPABASE_URL= @@ -608,3 +609,16 @@ AKASH_MANIFEST_VALIDATION_LEVEL=strict # Quai Network Ecosystem QUAI_PRIVATE_KEY= QUAI_RPC_URL=https://rpc.quai.network + +# Instagram Configuration +INSTAGRAM_DRY_RUN=false +INSTAGRAM_USERNAME= # Account username +INSTAGRAM_PASSWORD= # Account password +INSTAGRAM_APP_ID= # Instagram App ID is required +INSTAGRAM_APP_SECRET= # Instagram App Secret is required +INSTAGRAM_BUSINESS_ACCOUNT_ID= # Optional Business Account ID for additional features +INSTAGRAM_POST_INTERVAL_MIN=60 # Default: 60 minutes +INSTAGRAM_POST_INTERVAL_MAX=120 # Default: 120 minutes +INSTAGRAM_ENABLE_ACTION_PROCESSING=false # Enable/disable action processing +INSTAGRAM_ACTION_INTERVAL=5 # Interval between actions in minutes +INSTAGRAM_MAX_ACTIONS=1 # Maximum number of actions to process at once diff --git a/README.md b/README.md index df2bcc0018..30c430f5c4 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,9 @@ pnpm clean ### Interact via Browser -``` Once the agent is running, you should see the message to run "pnpm start:client" at the end. -Open another terminal and move to same directory and then run below command and follow the URL to chat to your agent. + +Open another terminal, move to same directory, run the command below, then follow the URL to chat with your agent. ```bash pnpm start:client diff --git a/README_TR.md b/README_TR.md index 0fc3935997..f4fd40cc94 100644 --- a/README_TR.md +++ b/README_TR.md @@ -37,7 +37,7 @@ - [Node.js 23+](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) - [pnpm](https://pnpm.io/installation) -> **Windows Kullanıcıları İçin Not:** WSL gereklidir +> **Windows Kullanıcıları İçin Not:** [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install-manual) gereklidir ### .env Dosyasını Düzenleyin diff --git a/agent/package.json b/agent/package.json index c4f1373978..24ea858f79 100644 --- a/agent/package.json +++ b/agent/package.json @@ -31,6 +31,7 @@ "@elizaos/client-lens": "workspace:*", "@elizaos/client-telegram": "workspace:*", "@elizaos/client-twitter": "workspace:*", + "@elizaos/client-instagram": "workspace:*", "@elizaos/client-slack": "workspace:*", "@elizaos/core": "workspace:*", "@elizaos/plugin-0g": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index 7f29971e89..b3882fc260 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -6,11 +6,12 @@ import { SqliteDatabaseAdapter } from "@elizaos/adapter-sqlite"; import { SupabaseDatabaseAdapter } from "@elizaos/adapter-supabase"; import { AutoClientInterface } from "@elizaos/client-auto"; import { DiscordClientInterface } from "@elizaos/client-discord"; -import { FarcasterClientInterface } from "@elizaos/client-farcaster"; +import { InstagramClientInterface } from "@elizaos/client-instagram"; import { LensAgentClient } from "@elizaos/client-lens"; import { SlackClientInterface } from "@elizaos/client-slack"; import { TelegramClientInterface } from "@elizaos/client-telegram"; import { TwitterClientInterface } from "@elizaos/client-twitter"; +import { FarcasterClientInterface } from "@elizaos/client-farcaster"; // import { ReclaimAdapter } from "@elizaos/plugin-reclaim"; import { PrimusAdapter } from "@elizaos/plugin-primus"; @@ -30,10 +31,10 @@ import { IDatabaseAdapter, IDatabaseCacheAdapter, ModelProviderName, + parseBooleanFromText, settings, stringToUuid, validateCharacterConfig, - parseBooleanFromText, } from "@elizaos/core"; import { zgPlugin } from "@elizaos/plugin-0g"; @@ -88,6 +89,7 @@ import { quaiPlugin } from "@elizaos/plugin-quai"; import { sgxPlugin } from "@elizaos/plugin-sgx"; import { solanaPlugin } from "@elizaos/plugin-solana"; import { solanaAgentkitPlguin } from "@elizaos/plugin-solana-agentkit"; +import { squidRouterPlugin } from "@elizaos/plugin-squid-router"; import { stargazePlugin } from "@elizaos/plugin-stargaze"; import { storyPlugin } from "@elizaos/plugin-story"; import { suiPlugin } from "@elizaos/plugin-sui"; @@ -97,7 +99,6 @@ import { teeMarlinPlugin } from "@elizaos/plugin-tee-marlin"; import { verifiableLogPlugin } from "@elizaos/plugin-tee-verifiable-log"; import { thirdwebPlugin } from "@elizaos/plugin-thirdweb"; import { tonPlugin } from "@elizaos/plugin-ton"; -import { squidRouterPlugin } from "@elizaos/plugin-squid-router"; import { webSearchPlugin } from "@elizaos/plugin-web-search"; import { echoChambersPlugin } from "@elizaos/plugin-echochambers"; import { dexScreenerPlugin } from "@elizaos/plugin-dexscreener"; @@ -622,6 +623,13 @@ export async function initializeClients( } } + if (clientTypes.includes(Clients.INSTAGRAM)) { + const instagramClient = await InstagramClientInterface.start(runtime); + if (instagramClient) { + clients.instagram = instagramClient; + } + } + if (clientTypes.includes(Clients.FARCASTER)) { const farcasterClient = await FarcasterClientInterface.start(runtime); if (farcasterClient) { diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index ec5a6a3f05..fdfd75e8da 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -1,6 +1,6 @@ import { type UUID, type Character } from "@elizaos/core"; -const BASE_URL = "http://localhost:3000"; +const BASE_URL = `http://localhost:${import.meta.env.VITE_SERVER_PORT}`; const fetcher = async ({ url, diff --git a/client/vite.config.ts b/client/vite.config.ts index c857927ca4..ecdb1abcb0 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import viteCompression from "vite-plugin-compression"; +import path from "path"; // https://vite.dev/config/ export default defineConfig({ @@ -13,6 +14,7 @@ export default defineConfig({ }), ], clearScreen: false, + envDir: path.resolve(__dirname, ".."), build: { outDir: "dist", minify: true, diff --git a/docs/README_FR.md b/docs/README_FR.md index 23c843db84..b9e798ea77 100644 --- a/docs/README_FR.md +++ b/docs/README_FR.md @@ -8,7 +8,7 @@ _Utilisée dans [@DegenSpartanAI](https://x.com/degenspartanai) et [@MarcAIndree - Ajout de multiples personnages avec [characterfile](https://github.com/lalalune/characterfile/) - Support des fonctionnalités et connecteurs Discord/ Twitter / Telegram, avec salons vocaux sur Discord - Accès aux données en mémoire et aux documents stockés -- Peut ouvrir et lire des documents PDF, retranscire des fichiers son et vidéo, résumer des conversations, etc. +- Peut ouvrir et lire des documents PDF, retranscrire des fichiers son et vidéo, résumer des conversations, etc. - Supporte les modèles open source et locaux (configuré par défaut avec Nous Hermes Llama 3.1B) - Supporte OpenAI pour une utilisation sur le cloud depuis une machine peu performante - Mode "Ask Claude" pour l'utilisation de Claude sur des modèles complexes @@ -22,10 +22,10 @@ _Utilisée dans [@DegenSpartanAI](https://x.com/degenspartanai) et [@MarcAIndree - [Node.js 22+](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) - [pnpm](https://pnpm.io/installation) -### Edit the .env file +### Éditer le fichier .env -- Copy .env.example to .env and fill in the appropriate values -- Edit the TWITTER environment variables to add your bot's username and password +- Copiez le fichier .env.example, renommez-le en .env et remplissez les valeurs appropriées. +- Modifiez les variables d'environnement TWITTER pour ajouter le nom d'utilisateur et le mot de passe de votre bot. ### Modifier les fichiers personnage @@ -50,7 +50,7 @@ pnpm clean # Personnaliser Eliza -### Ajouter un des actions personnalisées +### Ajouter une des actions personnalisées Pour éviter les conflits Git dans le répertoire core, nous vous recommandons d’ajouter les actions personnalisées dans un répertoire `custom_actions` et de les configurer dans le fichier `elizaConfig.yaml` . Vous pouvez consulter l’exemple dans le fichier `elizaConfig.example.yaml`. @@ -82,13 +82,13 @@ pnpm install --include=optional sharp Vous devez ajouter certaines variables à votre fichier .env pour vous connecter aux différentes plates-formes: ``` -# Variables d'environement Discord (nécessaires) +# Variables d'environnement Discord (nécessaires) DISCORD_APPLICATION_ID= DISCORD_API_TOKEN= # Bot token OPENAI_API_KEY=sk-* # OpenAI API key, starting with sk- ELEVENLABS_XI_API_KEY= # API key from elevenlabs -# Parmètres ELEVENLABS +# Paramètres ELEVENLABS ELEVENLABS_MODEL_ID=eleven_multilingual_v2 ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM ELEVENLABS_VOICE_STABILITY=0.5 @@ -144,7 +144,7 @@ Assurez-vous d’avoir le kit complet CUDA installé, y compris cuDNN et cuBLAS. ### Exécution locale Ajoutez XAI_MODEL et définissez-le à l’une des options ci-dessus [Run with -Llama](#run-with-llama) - Vous pouvez laisser les valeurs X_SERVER_URL et XAI_API_KEY vides, le modèle est sera téléchargé depuis huggingface et sera modifié en local +Llama](#run-with-llama) - Vous pouvez laisser les valeurs X_SERVER_URL et XAI_API_KEY vides, le modèle sera téléchargé depuis huggingface et sera modifié en local # Clients @@ -173,7 +173,7 @@ pnpm test:sqljs # Lance les tests avec SQL.js Les tests sont écrits en Jest et se trouvent ici : `src/**/*.test.ts`. L’environnement de test est configuré pour : - Charger les variables d’environnement depuis `.env.test` -- Ajouter d'un délai d'attente de 2 minutes pour les tests de longue durée +- Ajouter un délai d'attente de 2 minutes pour les tests de longue durée - Supporter les modules ESM - Lancer les tests de façon séquentielle (--runInBand) diff --git a/packages/adapter-sqlite/src/index.ts b/packages/adapter-sqlite/src/index.ts index 22252b8cf5..b18289be3c 100644 --- a/packages/adapter-sqlite/src/index.ts +++ b/packages/adapter-sqlite/src/index.ts @@ -16,6 +16,7 @@ import { type Relationship, type UUID, RAGKnowledgeItem, + type ChunkRow, } from "@elizaos/core"; import { Database } from "better-sqlite3"; import { v4 } from "uuid"; @@ -966,8 +967,106 @@ export class SqliteDatabaseAdapter } async removeKnowledge(id: UUID): Promise { - const sql = `DELETE FROM knowledge WHERE id = ?`; - this.db.prepare(sql).run(id); + if (typeof id !== "string") { + throw new Error("Knowledge ID must be a string"); + } + + try { + // Execute the transaction and ensure it's called with () + await this.db.transaction(() => { + if (id.includes("*")) { + const pattern = id.replace("*", "%"); + const sql = "DELETE FROM knowledge WHERE id LIKE ?"; + elizaLogger.debug( + `[Knowledge Remove] Executing SQL: ${sql} with pattern: ${pattern}` + ); + const stmt = this.db.prepare(sql); + const result = stmt.run(pattern); + elizaLogger.debug( + `[Knowledge Remove] Pattern deletion affected ${result.changes} rows` + ); + return result.changes; // Return changes for logging + } else { + // Log queries before execution + const selectSql = "SELECT id FROM knowledge WHERE id = ?"; + const chunkSql = + "SELECT id FROM knowledge WHERE json_extract(content, '$.metadata.originalId') = ?"; + elizaLogger.debug(`[Knowledge Remove] Checking existence with: + Main: ${selectSql} [${id}] + Chunks: ${chunkSql} [${id}]`); + + const mainEntry = this.db.prepare(selectSql).get(id) as + | ChunkRow + | undefined; + const chunks = this.db + .prepare(chunkSql) + .all(id) as ChunkRow[]; + + elizaLogger.debug(`[Knowledge Remove] Found:`, { + mainEntryExists: !!mainEntry?.id, + chunkCount: chunks.length, + chunkIds: chunks.map((c) => c.id), + }); + + // Execute and log chunk deletion + const chunkDeleteSql = + "DELETE FROM knowledge WHERE json_extract(content, '$.metadata.originalId') = ?"; + elizaLogger.debug( + `[Knowledge Remove] Executing chunk deletion: ${chunkDeleteSql} [${id}]` + ); + const chunkResult = this.db.prepare(chunkDeleteSql).run(id); + elizaLogger.debug( + `[Knowledge Remove] Chunk deletion affected ${chunkResult.changes} rows` + ); + + // Execute and log main entry deletion + const mainDeleteSql = "DELETE FROM knowledge WHERE id = ?"; + elizaLogger.debug( + `[Knowledge Remove] Executing main deletion: ${mainDeleteSql} [${id}]` + ); + const mainResult = this.db.prepare(mainDeleteSql).run(id); + elizaLogger.debug( + `[Knowledge Remove] Main deletion affected ${mainResult.changes} rows` + ); + + const totalChanges = + chunkResult.changes + mainResult.changes; + elizaLogger.debug( + `[Knowledge Remove] Total rows affected: ${totalChanges}` + ); + + // Verify deletion + const verifyMain = this.db.prepare(selectSql).get(id); + const verifyChunks = this.db.prepare(chunkSql).all(id); + elizaLogger.debug( + `[Knowledge Remove] Post-deletion check:`, + { + mainStillExists: !!verifyMain, + remainingChunks: verifyChunks.length, + } + ); + + return totalChanges; // Return changes for logging + } + })(); // Important: Call the transaction function + + elizaLogger.debug( + `[Knowledge Remove] Transaction completed for id: ${id}` + ); + } catch (error) { + elizaLogger.error("[Knowledge Remove] Error:", { + id, + error: + error instanceof Error + ? { + message: error.message, + stack: error.stack, + name: error.name, + } + : error, + }); + throw error; + } } async clearKnowledge(agentId: UUID, shared?: boolean): Promise { diff --git a/packages/client-github/README.md b/packages/client-github/README.md index 43bcc90ccb..17ec51f72b 100644 --- a/packages/client-github/README.md +++ b/packages/client-github/README.md @@ -47,7 +47,6 @@ const client = await GitHubClientInterface.start(runtime); // Convert repository files to agent memories await client.createMemoriesFromFiles(); -typescript // Convert repository files to agent memories await client.createMemoriesFromFiles(); ``` @@ -67,8 +66,6 @@ await client.createPullRequest( "Implements new functionality with tests" ); - -typescript await client.createPullRequest( "Feature: Add new functionality", "feature/new-feature", @@ -94,8 +91,6 @@ await client.createCommit( } ] ); - - ``` ## API Reference diff --git a/packages/client-instagram/README.md b/packages/client-instagram/README.md new file mode 100644 index 0000000000..6315f65dec --- /dev/null +++ b/packages/client-instagram/README.md @@ -0,0 +1,113 @@ +# @elizaos/client-instagram + +An Instagram client implementation for ElizaOS, enabling Instagram integration with support for media posting, comment handling, and interaction management. + +## Features + +- Instagram API integration using instagram-private-api +- Media post creation and scheduling +- Comment and interaction handling +- Profile management +- Media processing utilities +- Rate limiting and request queuing +- Session management and caching + +## Installation + +As this is a workspace package, it's installed as part of the ElizaOS monorepo: + +```bash +pnpm install +``` + +## Configuration + +The client requires the following environment variables: + +```bash +# Instagram Credentials +INSTAGRAM_USERNAME=your_username +INSTAGRAM_PASSWORD=your_password +INSTAGRAM_APP_ID=your_app_id +INSTAGRAM_APP_SECRET=your_app_secret + +# Optional Business Account +INSTAGRAM_BUSINESS_ACCOUNT_ID=your_business_account_id + +# Posting Configuration +POST_INTERVAL_MIN=90 # Minimum interval between posts (minutes) +POST_INTERVAL_MAX=180 # Maximum interval between posts (minutes) +ENABLE_ACTION_PROCESSING=true +ACTION_INTERVAL=5 # Minutes between action processing +MAX_ACTIONS_PROCESSING=1 # Maximum actions to process per interval +``` + +## Usage + +### Basic Initialization + +```typescript +import { InstagramClientInterface } from '@elizaos/client-instagram'; + +// Initialize the client +const instagramManager = await InstagramClientInterface.start(runtime); +``` + +### Posting Content + +All posts on Instagram must include media (image, video, or carousel): + +```typescript +// Post a single image +await instagramManager.post.createPost({ + media: [{ + type: 'IMAGE', + url: 'path/to/image.jpg' + }], + caption: 'Hello Instagram!' +}); + +// Post a carousel +await instagramManager.post.createPost({ + media: [ + { type: 'IMAGE', url: 'path/to/image1.jpg' }, + { type: 'IMAGE', url: 'path/to/image2.jpg' } + ], + caption: 'Check out these photos!' +}); +``` + +### Handling Interactions + +```typescript +// Handle comments +await instagramManager.interaction.handleComment({ + mediaId: 'media-123', + comment: 'Great post!', + userId: 'user-123' +}); + +// Like media +await instagramManager.interaction.likeMedia('media-123'); +``` + +## Key Components + +1. ClientBase + - Handles authentication and session management + - Manages API rate limiting + - Provides core API functionality + + +2. PostClient + - Manages media uploads + - Handles post scheduling + - Processes media before upload + + +3. InteractionClient + - Handles comments and likes + - Manages user interactions + - Processes notifications + + diff --git a/packages/client-instagram/package.json b/packages/client-instagram/package.json new file mode 100644 index 0000000000..6be4b3e5e4 --- /dev/null +++ b/packages/client-instagram/package.json @@ -0,0 +1,37 @@ +{ + "name": "@elizaos/client-instagram", + "version": "0.1.0", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "zod": "3.23.8", + "instagram-private-api": "^1.45.3", + "sharp": "^0.33.2", + "glob": "11.0.0" + }, + "devDependencies": { + "tsup": "8.3.5", + "@types/sharp": "^0.32.0" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache ." + } +} \ No newline at end of file diff --git a/packages/client-instagram/src/environment.ts b/packages/client-instagram/src/environment.ts new file mode 100644 index 0000000000..6795124876 --- /dev/null +++ b/packages/client-instagram/src/environment.ts @@ -0,0 +1,124 @@ +import { + IAgentRuntime, + parseBooleanFromText, +} from "@elizaos/core"; +import { z } from "zod"; + +export const DEFAULT_POST_INTERVAL_MIN = 60; +export const DEFAULT_POST_INTERVAL_MAX = 120; +export const DEFAULT_ACTION_INTERVAL = 5; +export const DEFAULT_MAX_ACTIONS = 1; + +// Define validation schemas for Instagram usernames and other fields +const instagramUsernameSchema = z + .string() + .min(1, "An Instagram Username must be at least 1 character long") + .max(30, "An Instagram Username cannot exceed 30 characters") + .refine((username) => { + // Instagram usernames can contain letters, numbers, periods, and underscores + return /^[A-Za-z0-9._]+$/.test(username); + }, "An Instagram Username can only contain letters, numbers, periods, and underscores"); + +/** + * Environment configuration schema for Instagram client + */ +export const instagramEnvSchema = z.object({ + INSTAGRAM_DRY_RUN: z.boolean(), + INSTAGRAM_USERNAME: instagramUsernameSchema, + INSTAGRAM_PASSWORD: z.string().min(1, "Instagram password is required"), + + // Instagram API credentials + INSTAGRAM_APP_ID: z.string().min(1, "Instagram App ID is required"), + INSTAGRAM_APP_SECRET: z.string().min(1, "Instagram App Secret is required"), + + // Optional Business Account ID for additional features + INSTAGRAM_BUSINESS_ACCOUNT_ID: z.string().optional(), + + // Posting configuration + INSTAGRAM_POST_INTERVAL_MIN: z.number().int().default(DEFAULT_POST_INTERVAL_MIN), + INSTAGRAM_POST_INTERVAL_MAX: z.number().int().default(DEFAULT_POST_INTERVAL_MAX), + + // Action processing configuration + INSTAGRAM_ENABLE_ACTION_PROCESSING: z.boolean().default(false), + INSTAGRAM_ACTION_INTERVAL: z.number().int().default(DEFAULT_ACTION_INTERVAL), + INSTAGRAM_MAX_ACTIONS: z.number().int().default(DEFAULT_MAX_ACTIONS), +}); + +export type InstagramConfig = z.infer; + +/** + * Validates and constructs an InstagramConfig object using zod, + * taking values from the IAgentRuntime or process.env as needed. + */ +export async function validateInstagramConfig( + runtime: IAgentRuntime +): Promise { + try { + const instagramConfig = { + INSTAGRAM_DRY_RUN: parseBooleanFromText( + runtime.getSetting("INSTAGRAM_DRY_RUN") || + process.env.INSTAGRAM_DRY_RUN + ) ?? false, + + INSTAGRAM_USERNAME: runtime.getSetting("INSTAGRAM_USERNAME") || + process.env.INSTAGRAM_USERNAME, + + INSTAGRAM_PASSWORD: runtime.getSetting("INSTAGRAM_PASSWORD") || + process.env.INSTAGRAM_PASSWORD, + + INSTAGRAM_APP_ID: runtime.getSetting("INSTAGRAM_APP_ID") || + process.env.INSTAGRAM_APP_ID, + + INSTAGRAM_APP_SECRET: runtime.getSetting("INSTAGRAM_APP_SECRET") || + process.env.INSTAGRAM_APP_SECRET, + + INSTAGRAM_BUSINESS_ACCOUNT_ID: runtime.getSetting("INSTAGRAM_BUSINESS_ACCOUNT_ID") || + process.env.INSTAGRAM_BUSINESS_ACCOUNT_ID, + + INSTAGRAM_POST_INTERVAL_MIN: parseInt( + runtime.getSetting("INSTAGRAM_POST_INTERVAL_MIN") || + process.env.INSTAGRAM_POST_INTERVAL_MIN || + DEFAULT_POST_INTERVAL_MIN.toString(), + 10 + ), + + INSTAGRAM_POST_INTERVAL_MAX: parseInt( + runtime.getSetting("INSTAGRAM_POST_INTERVAL_MAX") || + process.env.INSTAGRAM_POST_INTERVAL_MAX || + DEFAULT_POST_INTERVAL_MAX.toString(), + 10 + ), + + INSTAGRAM_ENABLE_ACTION_PROCESSING: parseBooleanFromText( + runtime.getSetting("INSTAGRAM_ENABLE_ACTION_PROCESSING") || + process.env.INSTAGRAM_ENABLE_ACTION_PROCESSING + ) ?? false, + + INSTAGRAM_ACTION_INTERVAL: parseInt( + runtime.getSetting("INSTAGRAM_ACTION_INTERVAL") || + process.env.INSTAGRAM_ACTION_INTERVAL || + DEFAULT_ACTION_INTERVAL.toString(), + 10 + ), + + INSTAGRAM_MAX_ACTIONS: parseInt( + runtime.getSetting("MAX_ACTIONS_PROCESSING") || + process.env.MAX_ACTIONS_PROCESSING || + DEFAULT_MAX_ACTIONS.toString(), + 10 + ), + }; + + return instagramEnvSchema.parse(instagramConfig); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `Instagram configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} \ No newline at end of file diff --git a/packages/client-instagram/src/index.ts b/packages/client-instagram/src/index.ts new file mode 100644 index 0000000000..9c70cb42a1 --- /dev/null +++ b/packages/client-instagram/src/index.ts @@ -0,0 +1,55 @@ +// src/index.ts +import { Client, IAgentRuntime, elizaLogger } from "@elizaos/core"; +import { validateInstagramConfig } from "./environment"; +import { initializeClient } from "./lib/auth"; +import { InstagramInteractionService } from "./services/interaction"; +import { InstagramPostService } from "./services/post"; + +export const InstagramClientInterface: Client = { + async start(runtime: IAgentRuntime) { + try { + // Validate configuration + const config = await validateInstagramConfig(runtime); + elizaLogger.log("Instagram client configuration validated"); + + // Initialize client and get initial state + const state = await initializeClient(runtime, config); + elizaLogger.log("Instagram client initialized"); + + // Create services + const postService = new InstagramPostService(runtime, state); + const interactionService = new InstagramInteractionService(runtime, state); + + // Start services + if (!config.INSTAGRAM_DRY_RUN) { + await postService.start(); + elizaLogger.log("Instagram post service started"); + + if (config.INSTAGRAM_ENABLE_ACTION_PROCESSING) { + await interactionService.start(); + elizaLogger.log("Instagram interaction service started"); + } + } else { + elizaLogger.log("Instagram client running in dry-run mode"); + } + + // Return manager instance + return { + post: postService, + interaction: interactionService, + state + }; + + } catch (error) { + elizaLogger.error("Failed to start Instagram client:", error); + throw error; + } + }, + + async stop(runtime: IAgentRuntime) { + elizaLogger.log("Stopping Instagram client services..."); + // Cleanup will be handled by the services themselves + } +}; + +export default InstagramClientInterface; \ No newline at end of file diff --git a/packages/client-instagram/src/lib/actions.ts b/packages/client-instagram/src/lib/actions.ts new file mode 100644 index 0000000000..0d134890b7 --- /dev/null +++ b/packages/client-instagram/src/lib/actions.ts @@ -0,0 +1,168 @@ +// src/lib/actions.ts +import { elizaLogger } from "@elizaos/core"; +import { Comment } from "../types"; +import { getIgClient } from "./state"; + +/** + * Fetches comments for a specific media post + */ +export async function fetchComments( + mediaId: string, + count: number = 20 +): Promise { + const ig = getIgClient(); + + try { + const feed = ig.feed.mediaComments(mediaId); + const comments = await feed.items(); + + return comments.slice(0, count).map(comment => ({ + id: comment.pk.toString(), + text: comment.text, + timestamp: new Date(comment.created_at * 1000).toISOString(), + username: comment.user.username, + replies: [] // Instagram API doesn't provide replies in the same call + })); + } catch (error) { + elizaLogger.error('Error fetching comments:', error); + throw error; + } +} + +/** + * Posts a comment on a media post + */ +export async function postComment( + mediaId: string, + text: string +): Promise { + const ig = getIgClient(); + + try { + const result = await ig.media.comment({ + mediaId, + text: text.slice(0, 2200) // Instagram comment length limit + }); + + return { + id: result.pk.toString(), + text: result.text, + timestamp: new Date(result.created_at * 1000).toISOString(), + username: result.user.username, + replies: [] + }; + } catch (error) { + elizaLogger.error('Error posting comment:', error); + throw error; + } +} + +/** + * Likes a media post + */ +export async function likeMedia(mediaId: string): Promise { + const ig = getIgClient(); + + try { + await ig.media.like({ + mediaId, + moduleInfo: { + module_name: 'profile', + user_id: ig.state.cookieUserId, + username: ig.state.cookieUsername + } + }); + elizaLogger.log(`Liked media: ${mediaId}`); + } catch (error) { + elizaLogger.error('Error liking media:', error); + throw error; + } +} + +/** + * Unlikes a media post + */ +export async function unlikeMedia(mediaId: string): Promise { + const ig = getIgClient(); + + try { + await ig.media.unlike({ + mediaId, + moduleInfo: { + module_name: 'profile', + user_id: ig.state.cookieUserId, + username: ig.state.cookieUsername + } + }); + elizaLogger.log(`Unliked media: ${mediaId}`); + } catch (error) { + elizaLogger.error('Error unliking media:', error); + throw error; + } +} + +/** + * Replies to a comment + */ +export async function replyToComment( + mediaId: string, + commentId: string, + text: string +): Promise { + const ig = getIgClient(); + + try { + const result = await ig.media.comment({ + mediaId, + text: text.slice(0, 2200), // Instagram comment length limit + replyToCommentId: commentId + }); + + return { + id: result.pk.toString(), + text: result.text, + timestamp: new Date(result.created_at * 1000).toISOString(), + username: result.user.username, + replies: [] + }; + } catch (error) { + elizaLogger.error('Error replying to comment:', error); + throw error; + } +} + +/** + * Deletes a comment + */ +export async function deleteComment( + mediaId: string, + commentId: string +): Promise { + const ig = getIgClient(); + + try { + await ig.media.deleteComment({ + mediaId, + commentId + }); + elizaLogger.log(`Deleted comment: ${commentId} from media: ${mediaId}`); + } catch (error) { + elizaLogger.error('Error deleting comment:', error); + throw error; + } +} + +/** + * Checks if current user has liked a media post + */ +export async function hasLikedMedia(mediaId: string): Promise { + const ig = getIgClient(); + + try { + const info = await ig.media.info(mediaId); + return info.items[0].has_liked ?? false; + } catch (error) { + elizaLogger.error('Error checking if media is liked:', error); + throw error; + } +} \ No newline at end of file diff --git a/packages/client-instagram/src/lib/auth.ts b/packages/client-instagram/src/lib/auth.ts new file mode 100644 index 0000000000..a2b956ce98 --- /dev/null +++ b/packages/client-instagram/src/lib/auth.ts @@ -0,0 +1,105 @@ +// src/lib/auth.ts +import { IAgentRuntime, elizaLogger } from "@elizaos/core"; +import { IgLoginTwoFactorRequiredError } from 'instagram-private-api'; +import { InstagramConfig } from "../environment"; +import { InstagramState } from "../types"; +import { fetchProfile } from "./profile"; +import { createInitialState, getIgClient } from "./state"; + +/** + * Authenticates with Instagram + */ +async function authenticate( + runtime: IAgentRuntime, + config: InstagramConfig +): Promise { + const ig = getIgClient(); + let state = createInitialState(); + + try { + // Generate device ID + ig.state.generateDevice(config.INSTAGRAM_USERNAME); + + // Attempt to load cached session + const cachedSession = await runtime.cacheManager.get('instagram/session'); + if (cachedSession) { + try { + await ig.state.deserialize(cachedSession); + const profile = await fetchProfile(runtime, config); + return { + ...state, + isInitialized: true, + profile + }; + } catch (error) { + elizaLogger.warn('Cached session invalid, proceeding with fresh login'); + } + } + + // Proceed with fresh login + try { + await ig.account.login( + config.INSTAGRAM_USERNAME, + config.INSTAGRAM_PASSWORD + ); + + // Cache the session + const serialized = await ig.state.serialize(); + await runtime.cacheManager.set('instagram/session', serialized); + + const profile = await fetchProfile(runtime, config); + + return { + ...state, + isInitialized: true, + profile + }; + + } catch (error) { + if (error instanceof IgLoginTwoFactorRequiredError) { + // Handle 2FA if needed - would need to implement 2FA code generation + throw new Error('2FA authentication not yet implemented'); + } + throw error; + } + + } catch (error) { + elizaLogger.error('Authentication failed:', error); + throw error; + } +} + +/** + * Sets up webhooks for real-time updates if needed + */ +async function setupWebhooks() { + // Implement webhook setup + // This is a placeholder for future implementation +} + +/** + * Initializes the Instagram client + */ +export async function initializeClient( + runtime: IAgentRuntime, + config: InstagramConfig +): Promise { + try { + // Authenticate and get initial state + const state = await authenticate(runtime, config); + + // Set up webhook handlers if needed + await setupWebhooks(); + + return state; + } catch (error) { + elizaLogger.error('Failed to initialize Instagram client:', error); + throw error; + } +} + +// Export other authentication related functions if needed +export { + authenticate, + setupWebhooks +}; diff --git a/packages/client-instagram/src/lib/media.ts b/packages/client-instagram/src/lib/media.ts new file mode 100644 index 0000000000..05d9532cc1 --- /dev/null +++ b/packages/client-instagram/src/lib/media.ts @@ -0,0 +1,38 @@ +import { elizaLogger, IAgentRuntime } from "@elizaos/core"; +import { InstagramConfig } from "../environment"; +import { MediaItem } from "../types"; +import { getIgClient } from "./state"; + +export async function fetchRecentMedia( + runtime: IAgentRuntime, + config: InstagramConfig, + count: number = 10 + ): Promise { + const ig = getIgClient(); + + try { + const feed = ig.feed.user(ig.state.cookieUserId); + const items = await feed.items(); + + return items.slice(0, count).map((item: any) => ({ + id: item.id, + mediaType: item.media_type as MediaItem['mediaType'], + mediaUrl: item.media_url, + thumbnailUrl: item.thumbnail_url || null, + permalink: item.permalink, + caption: item.caption?.text || null, + timestamp: item.timestamp, + children: item.children?.map((child: any) => ({ + id: child.id, + mediaType: child.media_type as MediaItem['mediaType'], + mediaUrl: child.media_url, + thumbnailUrl: child.thumbnail_url || null, + permalink: child.permalink, + timestamp: child.timestamp + })) || null + })); + } catch (error) { + elizaLogger.error('Error fetching recent media:', error); + throw error; + } + } \ No newline at end of file diff --git a/packages/client-instagram/src/lib/profile.ts b/packages/client-instagram/src/lib/profile.ts new file mode 100644 index 0000000000..a7b7fe9053 --- /dev/null +++ b/packages/client-instagram/src/lib/profile.ts @@ -0,0 +1,36 @@ +import { elizaLogger, IAgentRuntime } from "@elizaos/core"; +import { InstagramConfig } from "../environment"; +import { InstagramProfile } from "../types"; +import { getIgClient } from "./state"; + +export async function fetchProfile( + runtime: IAgentRuntime, + config: InstagramConfig + ): Promise { + const ig = getIgClient(); + + try { + const userInfo = await ig.user.info(ig.state.cookieUserId); + + const profile: InstagramProfile = { + id: userInfo.pk.toString(), + username: userInfo.username, + name: userInfo.full_name, + biography: userInfo.biography, + mediaCount: userInfo.media_count, + followerCount: userInfo.follower_count, + followingCount: userInfo.following_count + }; + + // Cache profile info + await runtime.cacheManager.set( + `instagram/profile/${config.INSTAGRAM_USERNAME}`, + profile + ); + + return profile; + } catch (error) { + elizaLogger.error('Error fetching profile:', error); + throw error; + } + } diff --git a/packages/client-instagram/src/lib/state.ts b/packages/client-instagram/src/lib/state.ts new file mode 100644 index 0000000000..0f756f38cc --- /dev/null +++ b/packages/client-instagram/src/lib/state.ts @@ -0,0 +1,21 @@ +import { IgApiClient } from 'instagram-private-api'; +import { InstagramState } from '../types'; + +// Create a singleton for the Instagram API client +let igClient: IgApiClient | null = null; + +export const getIgClient = () => { + if (!igClient) { + igClient = new IgApiClient(); + } + return igClient; +}; + +// Create initial state +export const createInitialState = (): InstagramState => ({ + accessToken: null, + longLivedToken: null, + profile: null, + isInitialized: false, + lastCheckedMediaId: null, +}); \ No newline at end of file diff --git a/packages/client-instagram/src/services/interaction.ts b/packages/client-instagram/src/services/interaction.ts new file mode 100644 index 0000000000..5b80fdee9c --- /dev/null +++ b/packages/client-instagram/src/services/interaction.ts @@ -0,0 +1,322 @@ +import { + composeContext, + elizaLogger, + generateText, + getEmbeddingZeroVector, + IAgentRuntime, + ModelClass, + stringToUuid, + UUID +} from "@elizaos/core"; +import { fetchComments, likeMedia, postComment } from "../lib/actions"; +import { getIgClient } from "../lib/state"; +import { InstagramState } from "../types"; + + // Templates + const instagramCommentTemplate = ` + # Areas of Expertise + {{knowledge}} + + # About {{agentName}} (@{{instagramUsername}}): + {{bio}} + {{lore}} + {{topics}} + + {{providers}} + + {{characterPostExamples}} + + {{postDirections}} + + # Task: Generate a response to the following Instagram comment in the voice and style of {{agentName}}. + Original Comment (@{{commentUsername}}): {{commentText}} + + Your response should be friendly, engaging, and natural. Keep it brief (1-2 sentences). + Do not use hashtags in comment responses. Be conversational and authentic.`; + + const shouldInteractTemplate = ` + # About {{agentName}} (@{{instagramUsername}}): + {{bio}} + {{lore}} + {{topics}} + + {{postDirections}} + + # Task: Determine if {{agentName}} should interact with this content: + Interaction Type: {{interactionType}} + User: @{{username}} + Content: {{content}} + + Consider: + 1. Is this user's content relevant to {{agentName}}'s interests? + 2. Would interaction be authentic and meaningful? + 3. Is there potential for valuable engagement? + + Respond with one of: + [INTERACT] - Content is highly relevant and engagement would be valuable + [SKIP] - Content is not relevant enough or engagement wouldn't be authentic + + Choose [INTERACT] only if very confident about relevance and value.`; + + export class InstagramInteractionService { + private runtime: IAgentRuntime; + private state: InstagramState; + private isProcessing: boolean = false; + private stopProcessing: boolean = false; + + constructor(runtime: IAgentRuntime, state: InstagramState) { + this.runtime = runtime; + this.state = state; + } + + async start() { + const handleInteractionsLoop = () => { + this.handleInteractions(); + if (!this.stopProcessing) { + setTimeout( + handleInteractionsLoop, + parseInt(this.runtime.getSetting('ACTION_INTERVAL') || '300', 10) * 1000 + ); + } + }; + + handleInteractionsLoop(); + } + + async stop() { + this.stopProcessing = true; + } + + private async generateResponse( + text: string, + username: string, + action: string + ) { + const state = await this.runtime.composeState( + { + userId: this.runtime.agentId, + roomId: stringToUuid(`instagram-temp-${Date.now()}-${this.runtime.agentId}`), + agentId: this.runtime.agentId, + content: { + text, + action, + }, + }, + { + instagramUsername: this.state.profile?.username, + commentUsername: username, + commentText: text, + } + ); + + const context = composeContext({ + state, + template: instagramCommentTemplate, + }); + + const response = await generateText({ + runtime: this.runtime, + context, + modelClass: ModelClass.SMALL, + }); + + return this.cleanResponse(response); + } + + private cleanResponse(response: string): string { + return response + .replace(/^\s*{?\s*"text":\s*"|"\s*}?\s*$/g, "") + .replace(/^['"](.*)['"]$/g, "$1") + .replace(/\\"/g, '"') + .trim(); + } + + private async handleInteractions() { + if (this.isProcessing) { + elizaLogger.log("Already processing interactions, skipping"); + return; + } + + try { + this.isProcessing = true; + elizaLogger.log("Checking Instagram interactions"); + + const ig = getIgClient(); + const activity = await ig.feed.news().items(); + + for (const item of activity) { + const activityId = `instagram-activity-${item.pk}`; + if (await this.runtime.cacheManager.get(activityId)) continue; + + switch (item.type) { + case 2: // Comment on your post + await this.handleComment(item); + break; + case 3: // Like on your post + await this.handleLike(item); + break; + case 12: // Mention in comment + await this.handleMention(item); + break; + } + + await this.runtime.cacheManager.set(activityId, true); + } + } catch (error) { + elizaLogger.error("Error handling Instagram interactions:", error); + } finally { + this.isProcessing = false; + } + } + + private async handleComment(item: any) { + try { + const comments = await fetchComments(item.media_id); + const comment = comments.find(c => c.id === item.pk.toString()); + if (!comment) return; + + const roomId = stringToUuid(`instagram-comment-${item.media_id}-${this.runtime.agentId}`); + const commentId = stringToUuid(`instagram-comment-${comment.id}-${this.runtime.agentId}`); + const userId = stringToUuid(`instagram-user-${item.user_id}-${this.runtime.agentId}`); + + const cleanedResponse = await this.generateResponse( + comment.text, + comment.username, + "COMMENT" + ); + + if (!cleanedResponse) { + elizaLogger.error("Failed to generate valid comment response"); + return; + } + + await this.ensureEntities(roomId, userId, comment.username); + await this.createInteractionMemories( + commentId, + userId, + roomId, + comment, + cleanedResponse, + item.media_id + ); + + } catch (error) { + elizaLogger.error("Error handling comment:", error); + } + } + + private async handleLike(item: any) { + try { + const state = await this.runtime.composeState( + { + userId: this.runtime.agentId, + roomId: stringToUuid(`instagram-like-${item.media_id}-${this.runtime.agentId}`), + agentId: this.runtime.agentId, + content: { text: "", action: "DECIDE_INTERACTION" }, + }, + { + instagramUsername: this.state.profile?.username, + interactionType: "like", + username: item.user?.username, + content: item.text || "", + } + ); + + const context = composeContext({ state, template: shouldInteractTemplate }); + const decision = await generateText({ + runtime: this.runtime, + context, + modelClass: ModelClass.SMALL, + }); + + if (decision.includes("[INTERACT]")) { + const userFeed = await getIgClient().feed.user(item.user_id).items(); + if (userFeed.length > 0) { + await likeMedia(userFeed[0].id); + elizaLogger.log(`Liked post from user: ${item.user?.username}`); + } + } + } catch (error) { + elizaLogger.error("Error handling like:", error); + } + } + + private async handleMention(item: any) { + try { + const roomId = stringToUuid(`instagram-mention-${item.media_id}-${this.runtime.agentId}`); + const mentionId = stringToUuid(`instagram-mention-${item.pk}-${this.runtime.agentId}`); + const userId = stringToUuid(`instagram-user-${item.user.pk}-${this.runtime.agentId}`); + + const cleanedResponse = await this.generateResponse( + item.text, + item.user.username, + "MENTION" + ); + + if (!cleanedResponse) { + elizaLogger.error("Failed to generate valid mention response"); + return; + } + + await this.ensureEntities(roomId, userId, item.user.username); + await this.createInteractionMemories( + mentionId, + userId, + roomId, + item, + cleanedResponse, + item.media_id + ); + + } catch (error) { + elizaLogger.error("Error handling mention:", error); + } + } + + private async ensureEntities(roomId: UUID, userId: UUID, username: string) { + await this.runtime.ensureRoomExists(roomId); + await this.runtime.ensureUserExists(userId, username, username, "instagram"); + await this.runtime.ensureParticipantInRoom(this.runtime.agentId, roomId); + } + + private async createInteractionMemories( + originalId: UUID, + userId: UUID, + roomId: UUID, + originalItem: any, + response: string, + mediaId: string + ) { + // Create memory of original interaction + await this.runtime.messageManager.createMemory({ + id: originalId, + userId, + agentId: this.runtime.agentId, + content: { + text: originalItem.text, + source: "instagram", + }, + roomId, + embedding: getEmbeddingZeroVector(), + createdAt: new Date(originalItem.timestamp || originalItem.created_at * 1000).getTime(), + }); + + // Post response + const postedComment = await postComment(mediaId, response); + + // Create memory of our response + await this.runtime.messageManager.createMemory({ + id: stringToUuid(`instagram-reply-${postedComment.id}-${this.runtime.agentId}`), + userId: this.runtime.agentId, + agentId: this.runtime.agentId, + content: { + text: response, + source: "instagram", + inReplyTo: originalId + }, + roomId, + embedding: getEmbeddingZeroVector(), + createdAt: Date.now(), + }); + } + } \ No newline at end of file diff --git a/packages/client-instagram/src/services/post.ts b/packages/client-instagram/src/services/post.ts new file mode 100644 index 0000000000..2d1a94d9b5 --- /dev/null +++ b/packages/client-instagram/src/services/post.ts @@ -0,0 +1,328 @@ +// src/services/post.ts +import { IAgentRuntime, ModelClass, composeContext, elizaLogger, generateImage, generateText, getEmbeddingZeroVector, stringToUuid } from "@elizaos/core"; +import { promises as fs } from 'fs'; +import path from "path"; +import sharp from 'sharp'; +import { getIgClient } from "../lib/state"; +import { InstagramState } from "../types"; + +// Template for generating Instagram posts +const instagramPostTemplate = ` +# Areas of Expertise +{{knowledge}} + +# About {{agentName}} (@{{instagramUsername}}): +{{bio}} +{{lore}} +{{topics}} + +{{providers}} + +{{characterPostExamples}} + +{{postDirections}} + +# Task: Generate a post in the voice and style and perspective of {{agentName}}. +Write a post that is {{adjective}} about {{topic}} (without mentioning {{topic}} directly), from the perspective of {{agentName}}. +Your response should be 1-3 sentences (choose the length at random). +Your response should not contain any questions. Brief, concise statements only. +Add up to 3 relevant hashtags at the end.`; + +interface PostOptions { + media: Array<{ + type: 'IMAGE' | 'VIDEO' | 'CAROUSEL'; + url: string; + }>; + caption?: string; +} + +export class InstagramPostService { + private runtime: IAgentRuntime; + private state: InstagramState; + private isProcessing: boolean = false; + private lastPostTime: number = 0; + private stopProcessing: boolean = false; + + constructor(runtime: IAgentRuntime, state: InstagramState) { + this.runtime = runtime; + this.state = state; + } + + async start() { + const generatePostLoop = async () => { + const lastPost = await this.runtime.cacheManager.get<{ timestamp: number }>( + 'instagram/lastPost' + ); + + const lastPostTimestamp = lastPost?.timestamp ?? 0; + const minMinutes = parseInt(this.runtime.getSetting('POST_INTERVAL_MIN') || '90', 10); + const maxMinutes = parseInt(this.runtime.getSetting('POST_INTERVAL_MAX') || '180', 10); + const randomMinutes = Math.floor(Math.random() * (maxMinutes - minMinutes + 1)) + minMinutes; + const delay = randomMinutes * 60 * 1000; + + if (Date.now() > lastPostTimestamp + delay) { + await this.generateNewPost(); + } + + if (!this.stopProcessing) { + setTimeout(generatePostLoop, delay); + } + + elizaLogger.log(`Next Instagram post scheduled in ${randomMinutes} minutes`); + }; + + // Start the loop + generatePostLoop(); + } + + async stop() { + this.stopProcessing = true; + } + + private async generateNewPost() { + try { + elizaLogger.log("Generating new Instagram post"); + + const roomId = stringToUuid( + "instagram_generate_room-" + this.state.profile?.username + ); + + await this.runtime.ensureUserExists( + this.runtime.agentId, + this.state.profile?.username || '', + this.runtime.character.name, + "instagram" + ); + + const topics = this.runtime.character.topics.join(", "); + + const state = await this.runtime.composeState( + { + userId: this.runtime.agentId, + roomId: roomId, + agentId: this.runtime.agentId, + content: { + text: topics || "", + action: "POST", + }, + }, + { + instagramUsername: this.state.profile?.username + } + ); + + const context = composeContext({ + state, + // TODO: Add back in when we have a template for Instagram on character + //template: this.runtime.character.templates?.instagramPostTemplate || instagramPostTemplate, + template: instagramPostTemplate, + + }); + + elizaLogger.debug("generate post prompt:\n" + context); + + const content = await generateText({ + runtime: this.runtime, + context, + modelClass: ModelClass.SMALL, + }); + + // Clean the generated content + let cleanedContent = ""; + + // Try parsing as JSON first + try { + const parsedResponse = JSON.parse(content); + if (parsedResponse.text) { + cleanedContent = parsedResponse.text; + } else if (typeof parsedResponse === "string") { + cleanedContent = parsedResponse; + } + } catch (error) { + // If not JSON, clean the raw content + cleanedContent = content + .replace(/^\s*{?\s*"text":\s*"|"\s*}?\s*$/g, "") // Remove JSON-like wrapper + .replace(/^['"](.*)['"]$/g, "$1") // Remove quotes + .replace(/\\"/g, '"') // Unescape quotes + .replace(/\\n/g, "\n\n") // Unescape newlines + .trim(); + } + + if (!cleanedContent) { + elizaLogger.error("Failed to extract valid content from response:", { + rawResponse: content, + attempted: "JSON parsing", + }); + return; + } + + // For Instagram, we need to generate or get an image + const mediaUrl = await this.getOrGenerateImage(cleanedContent); + + await this.createPost({ + media: [{ + type: 'IMAGE', + url: mediaUrl + }], + caption: cleanedContent + }); + + // Create memory of the post + await this.runtime.messageManager.createMemory({ + id: stringToUuid(`instagram-post-${Date.now()}`), + userId: this.runtime.agentId, + agentId: this.runtime.agentId, + content: { + text: cleanedContent, + source: "instagram", + }, + roomId, + embedding: getEmbeddingZeroVector(), + createdAt: Date.now(), + }); + + } catch (error) { + elizaLogger.error("Error generating Instagram post:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: 'generateNewPost' + }); + } + } + + // Placeholder - implement actual image generation/selection + private async getOrGenerateImage(content: string): Promise { + try { + elizaLogger.log("Generating image for Instagram post"); + + const result = await generateImage({ + prompt: content, + width: 1024, + height: 1024, + count: 1, + numIterations: 50, + guidanceScale: 7.5 + }, this.runtime); + + if (!result.success || !result.data || result.data.length === 0) { + throw new Error("Failed to generate image: " + (result.error || "No image data returned")); + } + + // Save the base64 image to a temporary file + const imageData = result.data[0].replace(/^data:image\/\w+;base64,/, ''); + const tempDir = path.resolve(process.cwd(), 'temp'); + await fs.mkdir(tempDir, { recursive: true }); + const tempFile = path.join(tempDir, `instagram-post-${Date.now()}.png`); + await fs.writeFile(tempFile, Buffer.from(imageData, 'base64')); + + return tempFile; + } catch (error) { + elizaLogger.error("Error generating image:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: 'getOrGenerateImage' + }); + throw error; + } + } + + async createPost(options: PostOptions) { + const ig = getIgClient(); + + try { + elizaLogger.log("Creating Instagram post", { + mediaCount: options.media.length, + hasCaption: !!options.caption + }); + + // Process media + const processedMedia = await Promise.all( + options.media.map(async (media) => { + const buffer = await this.processMedia(media); + return { + ...media, + buffer + }; + }) + ); + + // Handle different post types + if (processedMedia.length > 1) { + // Create carousel post + await ig.publish.album({ + items: processedMedia.map(media => ({ + file: media.buffer, + caption: options.caption + })) + }); + } else { + // Single image/video post + const media = processedMedia[0]; + if (media.type === 'VIDEO') { + await ig.publish.video({ + video: media.buffer, + caption: options.caption, + coverImage: media.buffer + }); + } else { + await ig.publish.photo({ + file: media.buffer, + caption: options.caption + }); + } + } + + // Update last post time + this.lastPostTime = Date.now(); + await this.runtime.cacheManager.set('instagram/lastPost', { + timestamp: this.lastPostTime + }); + + elizaLogger.log("Instagram post created successfully"); + } catch (error) { + elizaLogger.error("Error creating Instagram post:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: 'createPost', + mediaCount: options.media.length, + hasCaption: !!options.caption + }); + throw error; + } + } + + private async processMedia(media: { type: string; url: string }): Promise { + try { + elizaLogger.log("Processing media", { type: media.type, url: media.url }); + + // Read file directly from filesystem instead of using fetch + const buffer = await fs.readFile(media.url); + + if (media.type === 'IMAGE') { + // Process image with sharp + return await sharp(buffer) + .resize(1080, 1080, { + fit: 'inside', + withoutEnlargement: true + }) + .jpeg({ + quality: 85, + progressive: true + }) + .toBuffer(); + } + + // For other types, return original buffer + return buffer; + } catch (error) { + elizaLogger.error("Error processing media:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: 'processMedia', + mediaType: media.type, + url: media.url + }); + throw error; + } + } +} \ No newline at end of file diff --git a/packages/client-instagram/src/types.ts b/packages/client-instagram/src/types.ts new file mode 100644 index 0000000000..e34a3a8c24 --- /dev/null +++ b/packages/client-instagram/src/types.ts @@ -0,0 +1,37 @@ + +export interface InstagramState { + accessToken: string | null; + longLivedToken: string | null; + profile: InstagramProfile | null; + isInitialized: boolean; + lastCheckedMediaId: string | null; +} + +export interface InstagramProfile { + id: string; + username: string; + name: string; + biography: string; + mediaCount: number; + followerCount: number; + followingCount: number; +} + +export interface MediaItem { + id: string; + mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM'; + mediaUrl: string; + thumbnailUrl?: string; + permalink: string; + caption?: string; + timestamp: string; + children?: MediaItem[]; +} + +export interface Comment { + id: string; + text: string; + timestamp: string; + username: string; + replies?: Comment[]; +} \ No newline at end of file diff --git a/packages/client-instagram/tsconfig.json b/packages/client-instagram/tsconfig.json new file mode 100644 index 0000000000..73993deaaf --- /dev/null +++ b/packages/client-instagram/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/client-instagram/tsup.config.ts b/packages/client-instagram/tsup.config.ts new file mode 100644 index 0000000000..8cba0c2089 --- /dev/null +++ b/packages/client-instagram/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + external: [ + "sharp", + "fs", + "path", + "instagram-private-api", + // Add other externals as needed + ], +}); \ No newline at end of file diff --git a/packages/core/src/environment.ts b/packages/core/src/environment.ts index 4bbe5fcb91..9d51e0f4e6 100644 --- a/packages/core/src/environment.ts +++ b/packages/core/src/environment.ts @@ -77,15 +77,23 @@ export const CharacterSchema = z.object({ postExamples: z.array(z.string()), topics: z.array(z.string()), adjectives: z.array(z.string()), - knowledge: z.array( - z.union([ - z.string(), - z.object({ - path: z.string(), - shared: z.boolean().optional() - }) - ]) - ).optional(), + knowledge: z + .array( + z.union([ + z.string(), // Direct knowledge strings + z.object({ + // Individual file config + path: z.string(), + shared: z.boolean().optional(), + }), + z.object({ + // Directory config + directory: z.string(), + shared: z.boolean().optional(), + }), + ]) + ) + .optional(), clients: z.array(z.nativeEnum(Clients)), plugins: z.union([z.array(z.string()), z.array(PluginSchema)]), settings: z diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts index 0cd836a5d5..57c47de256 100644 --- a/packages/core/src/generation.ts +++ b/packages/core/src/generation.ts @@ -52,7 +52,7 @@ import { import { fal } from "@fal-ai/client"; import BigNumber from "bignumber.js"; -import {createPublicClient, http} from "viem"; +import { createPublicClient, http } from "viem"; type Tool = CoreTool; type StepResult = AIStepResult; @@ -169,20 +169,41 @@ async function truncateTiktoken( * Get OnChain EternalAI System Prompt * @returns System Prompt */ -async function getOnChainEternalAISystemPrompt(runtime: IAgentRuntime): Promise | undefined { - const agentId = runtime.getSetting("ETERNALAI_AGENT_ID") +async function getOnChainEternalAISystemPrompt( + runtime: IAgentRuntime +): Promise | undefined { + const agentId = runtime.getSetting("ETERNALAI_AGENT_ID"); const providerUrl = runtime.getSetting("ETERNALAI_RPC_URL"); - const contractAddress = runtime.getSetting("ETERNALAI_AGENT_CONTRACT_ADDRESS"); + const contractAddress = runtime.getSetting( + "ETERNALAI_AGENT_CONTRACT_ADDRESS" + ); if (agentId && providerUrl && contractAddress) { // get on-chain system-prompt - const contractABI = [{"inputs": [{"internalType": "uint256", "name": "_agentId", "type": "uint256"}], "name": "getAgentSystemPrompt", "outputs": [{"internalType": "bytes[]", "name": "","type": "bytes[]"}], "stateMutability": "view", "type": "function"}]; + const contractABI = [ + { + inputs: [ + { + internalType: "uint256", + name: "_agentId", + type: "uint256", + }, + ], + name: "getAgentSystemPrompt", + outputs: [ + { internalType: "bytes[]", name: "", type: "bytes[]" }, + ], + stateMutability: "view", + type: "function", + }, + ]; const publicClient = createPublicClient({ transport: http(providerUrl), }); try { - const validAddress: `0x${string}` = contractAddress as `0x${string}`; + const validAddress: `0x${string}` = + contractAddress as `0x${string}`; const result = await publicClient.readContract({ address: validAddress, abi: contractABI, @@ -190,17 +211,17 @@ async function getOnChainEternalAISystemPrompt(runtime: IAgentRuntime): Promise< args: [new BigNumber(agentId)], }); if (result) { - elizaLogger.info('on-chain system-prompt response', result[0]); + elizaLogger.info("on-chain system-prompt response", result[0]); const value = result[0].toString().replace("0x", ""); - let content = Buffer.from(value, 'hex').toString('utf-8'); - elizaLogger.info('on-chain system-prompt', content); - return await fetchEternalAISystemPrompt(runtime, content) + const content = Buffer.from(value, "hex").toString("utf-8"); + elizaLogger.info("on-chain system-prompt", content); + return await fetchEternalAISystemPrompt(runtime, content); } else { return undefined; } } catch (error) { elizaLogger.error(error); - elizaLogger.error('err', error); + elizaLogger.error("err", error); } } return undefined; @@ -210,34 +231,42 @@ async function getOnChainEternalAISystemPrompt(runtime: IAgentRuntime): Promise< * Fetch EternalAI System Prompt * @returns System Prompt */ -async function fetchEternalAISystemPrompt(runtime: IAgentRuntime, content: string): Promise | undefined { - const IPFS = "ipfs://" +async function fetchEternalAISystemPrompt( + runtime: IAgentRuntime, + content: string +): Promise | undefined { + const IPFS = "ipfs://"; const containsSubstring: boolean = content.includes(IPFS); if (containsSubstring) { - - const lightHouse = content.replace(IPFS, "https://gateway.lighthouse.storage/ipfs/"); - elizaLogger.info("fetch lightHouse", lightHouse) + const lightHouse = content.replace( + IPFS, + "https://gateway.lighthouse.storage/ipfs/" + ); + elizaLogger.info("fetch lightHouse", lightHouse); const responseLH = await fetch(lightHouse, { method: "GET", }); - elizaLogger.info("fetch lightHouse resp", responseLH) + elizaLogger.info("fetch lightHouse resp", responseLH); if (responseLH.ok) { const data = await responseLH.text(); return data; } else { - const gcs = content.replace(IPFS, "https://cdn.eternalai.org/upload/") - elizaLogger.info("fetch gcs", gcs) + const gcs = content.replace( + IPFS, + "https://cdn.eternalai.org/upload/" + ); + elizaLogger.info("fetch gcs", gcs); const responseGCS = await fetch(gcs, { method: "GET", }); - elizaLogger.info("fetch lightHouse gcs", responseGCS) + elizaLogger.info("fetch lightHouse gcs", responseGCS); if (responseGCS.ok) { const data = await responseGCS.text(); return data; } else { - throw new Error("invalid on-chain system prompt") + throw new Error("invalid on-chain system prompt"); } - return undefined + return undefined; } } else { return content; @@ -250,8 +279,12 @@ async function fetchEternalAISystemPrompt(runtime: IAgentRuntime, content: strin * @param provider The model provider name * @returns The Cloudflare Gateway base URL if enabled, undefined otherwise */ -function getCloudflareGatewayBaseURL(runtime: IAgentRuntime, provider: string): string | undefined { - const isCloudflareEnabled = runtime.getSetting("CLOUDFLARE_GW_ENABLED") === "true"; +function getCloudflareGatewayBaseURL( + runtime: IAgentRuntime, + provider: string +): string | undefined { + const isCloudflareEnabled = + runtime.getSetting("CLOUDFLARE_GW_ENABLED") === "true"; const cloudflareAccountId = runtime.getSetting("CLOUDFLARE_AI_ACCOUNT_ID"); const cloudflareGatewayId = runtime.getSetting("CLOUDFLARE_AI_GATEWAY_ID"); @@ -259,7 +292,7 @@ function getCloudflareGatewayBaseURL(runtime: IAgentRuntime, provider: string): isEnabled: isCloudflareEnabled, hasAccountId: !!cloudflareAccountId, hasGatewayId: !!cloudflareGatewayId, - provider: provider + provider: provider, }); if (!isCloudflareEnabled) { @@ -268,12 +301,16 @@ function getCloudflareGatewayBaseURL(runtime: IAgentRuntime, provider: string): } if (!cloudflareAccountId) { - elizaLogger.warn("Cloudflare Gateway is enabled but CLOUDFLARE_AI_ACCOUNT_ID is not set"); + elizaLogger.warn( + "Cloudflare Gateway is enabled but CLOUDFLARE_AI_ACCOUNT_ID is not set" + ); return undefined; } if (!cloudflareGatewayId) { - elizaLogger.warn("Cloudflare Gateway is enabled but CLOUDFLARE_AI_GATEWAY_ID is not set"); + elizaLogger.warn( + "Cloudflare Gateway is enabled but CLOUDFLARE_AI_GATEWAY_ID is not set" + ); return undefined; } @@ -282,7 +319,7 @@ function getCloudflareGatewayBaseURL(runtime: IAgentRuntime, provider: string): provider, baseURL, accountId: cloudflareAccountId, - gatewayId: cloudflareGatewayId + gatewayId: cloudflareGatewayId, }); return baseURL; @@ -372,9 +409,13 @@ export async function generateText({ hasRuntime: !!runtime, runtimeSettings: { CLOUDFLARE_GW_ENABLED: runtime.getSetting("CLOUDFLARE_GW_ENABLED"), - CLOUDFLARE_AI_ACCOUNT_ID: runtime.getSetting("CLOUDFLARE_AI_ACCOUNT_ID"), - CLOUDFLARE_AI_GATEWAY_ID: runtime.getSetting("CLOUDFLARE_AI_GATEWAY_ID") - } + CLOUDFLARE_AI_ACCOUNT_ID: runtime.getSetting( + "CLOUDFLARE_AI_ACCOUNT_ID" + ), + CLOUDFLARE_AI_GATEWAY_ID: runtime.getSetting( + "CLOUDFLARE_AI_GATEWAY_ID" + ), + }, }); const endpoint = @@ -494,8 +535,11 @@ export async function generateText({ case ModelProviderName.TOGETHER: case ModelProviderName.NINETEEN_AI: case ModelProviderName.AKASH_CHAT_API: { - elizaLogger.debug("Initializing OpenAI model with Cloudflare check"); - const baseURL = getCloudflareGatewayBaseURL(runtime, 'openai') || endpoint; + elizaLogger.debug( + "Initializing OpenAI model with Cloudflare check" + ); + const baseURL = + getCloudflareGatewayBaseURL(runtime, "openai") || endpoint; //elizaLogger.debug("OpenAI baseURL result:", { baseURL }); const openai = createOpenAI({ @@ -531,20 +575,23 @@ export async function generateText({ const openai = createOpenAI({ apiKey, baseURL: endpoint, - fetch: async (url: string, options: any) => { + fetch: async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const url = typeof input === 'string' ? input : input.toString(); const chain_id = runtime.getSetting("ETERNALAI_CHAIN_ID") || "45762"; + + const options: RequestInit = { ...init }; if (options?.body) { - const body = JSON.parse(options.body); + const body = JSON.parse(options.body as string); body.chain_id = chain_id; options.body = JSON.stringify(body); } + const fetching = await runtime.fetch(url, options); - if ( - parseBooleanFromText( - runtime.getSetting("ETERNALAI_LOG") - ) - ) { + + if (parseBooleanFromText( + runtime.getSetting("ETERNALAI_LOG") + )) { elizaLogger.info( "Request data: ", JSON.stringify(options, null, 2) @@ -565,17 +612,26 @@ export async function generateText({ }, }); - let system_prompt = runtime.character.system ?? settings.SYSTEM_PROMPT ?? undefined; + let system_prompt = + runtime.character.system ?? + settings.SYSTEM_PROMPT ?? + undefined; try { - const on_chain_system_prompt = await getOnChainEternalAISystemPrompt(runtime); + const on_chain_system_prompt = + await getOnChainEternalAISystemPrompt(runtime); if (!on_chain_system_prompt) { - elizaLogger.error(new Error("invalid on_chain_system_prompt")) + elizaLogger.error( + new Error("invalid on_chain_system_prompt") + ); } else { - system_prompt = on_chain_system_prompt - elizaLogger.info("new on-chain system prompt", system_prompt) + system_prompt = on_chain_system_prompt; + elizaLogger.info( + "new on-chain system prompt", + system_prompt + ); } } catch (e) { - elizaLogger.error(e) + elizaLogger.error(e); } const { text: openaiResponse } = await aiGenerateText({ @@ -643,11 +699,19 @@ export async function generateText({ } case ModelProviderName.ANTHROPIC: { - elizaLogger.debug("Initializing Anthropic model with Cloudflare check"); - const baseURL = getCloudflareGatewayBaseURL(runtime, 'anthropic') || "https://api.anthropic.com/v1"; + elizaLogger.debug( + "Initializing Anthropic model with Cloudflare check" + ); + const baseURL = + getCloudflareGatewayBaseURL(runtime, "anthropic") || + "https://api.anthropic.com/v1"; elizaLogger.debug("Anthropic baseURL result:", { baseURL }); - const anthropic = createAnthropic({ apiKey, baseURL, fetch: runtime.fetch }); + const anthropic = createAnthropic({ + apiKey, + baseURL, + fetch: runtime.fetch, + }); const { text: anthropicResponse } = await aiGenerateText({ model: anthropic.languageModel(model), prompt: context, @@ -735,10 +799,16 @@ export async function generateText({ } case ModelProviderName.GROQ: { - elizaLogger.debug("Initializing Groq model with Cloudflare check"); - const baseURL = getCloudflareGatewayBaseURL(runtime, 'groq'); + elizaLogger.debug( + "Initializing Groq model with Cloudflare check" + ); + const baseURL = getCloudflareGatewayBaseURL(runtime, "groq"); elizaLogger.debug("Groq baseURL result:", { baseURL }); - const groq = createGroq({ apiKey, fetch: runtime.fetch, baseURL }); + const groq = createGroq({ + apiKey, + fetch: runtime.fetch, + baseURL, + }); const { text: groqResponse } = await aiGenerateText({ model: groq.languageModel(model), @@ -1177,12 +1247,22 @@ export async function splitChunks( chunkSize: number = 512, bleed: number = 20 ): Promise { + elizaLogger.debug(`[splitChunks] Starting text split`); + const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: Number(chunkSize), chunkOverlap: Number(bleed), }); - return textSplitter.splitText(content); + const chunks = await textSplitter.splitText(content); + elizaLogger.debug(`[splitChunks] Split complete:`, { + numberOfChunks: chunks.length, + averageChunkSize: + chunks.reduce((acc, chunk) => acc + chunk.length, 0) / + chunks.length, + }); + + return chunks; } /** @@ -1195,7 +1275,6 @@ export async function splitChunks( * @param opts.presence_penalty The presence penalty to apply (0.0 to 2.0) * @param opts.temperature The temperature to control randomness (0.0 to 2.0) * @param opts.serverUrl The URL of the API server - * @param opts.token The API token for authentication * @param opts.max_context_length Maximum allowed context length in tokens * @param opts.max_response_length Maximum allowed response length in tokens * @returns Promise resolving to a boolean value parsed from the model's response @@ -2020,7 +2099,9 @@ async function handleOpenAI({ provider: _provider, runtime, }: ProviderOptions): Promise> { - const baseURL = getCloudflareGatewayBaseURL(runtime, 'openai') || models.openai.endpoint; + const baseURL = + getCloudflareGatewayBaseURL(runtime, "openai") || + models.openai.endpoint; const openai = createOpenAI({ apiKey, baseURL }); return await aiGenerateObject({ model: openai.languageModel(model), @@ -2049,7 +2130,7 @@ async function handleAnthropic({ runtime, }: ProviderOptions): Promise> { elizaLogger.debug("Handling Anthropic request with Cloudflare check"); - const baseURL = getCloudflareGatewayBaseURL(runtime, 'anthropic'); + const baseURL = getCloudflareGatewayBaseURL(runtime, "anthropic"); elizaLogger.debug("Anthropic handleAnthropic baseURL:", { baseURL }); const anthropic = createAnthropic({ apiKey, baseURL }); @@ -2106,7 +2187,7 @@ async function handleGroq({ runtime, }: ProviderOptions): Promise> { elizaLogger.debug("Handling Groq request with Cloudflare check"); - const baseURL = getCloudflareGatewayBaseURL(runtime, 'groq'); + const baseURL = getCloudflareGatewayBaseURL(runtime, "groq"); elizaLogger.debug("Groq handleGroq baseURL:", { baseURL }); const groq = createGroq({ apiKey, baseURL }); diff --git a/packages/core/src/localembeddingManager.ts b/packages/core/src/localembeddingManager.ts index e6f853a934..e8741041f6 100644 --- a/packages/core/src/localembeddingManager.ts +++ b/packages/core/src/localembeddingManager.ts @@ -104,17 +104,17 @@ class LocalEmbeddingModelManager { try { // Let fastembed handle tokenization internally const embedding = await this.model.queryEmbed(input); - // Debug the raw embedding - elizaLogger.debug("Raw embedding from BGE:", { - type: typeof embedding, - isArray: Array.isArray(embedding), - dimensions: Array.isArray(embedding) - ? embedding.length - : "not an array", - sample: Array.isArray(embedding) - ? embedding.slice(0, 5) - : embedding, - }); + // Debug the raw embedding - uncomment if debugging embeddings + // elizaLogger.debug("Raw embedding from BGE:", { + // type: typeof embedding, + // isArray: Array.isArray(embedding), + // dimensions: Array.isArray(embedding) + // ? embedding.length + // : "not an array", + // sample: Array.isArray(embedding) + // ? embedding.slice(0, 5) + // : embedding, + // }); return this.processEmbedding(embedding); } catch (error) { elizaLogger.error("Embedding generation failed:", error); diff --git a/packages/core/src/ragknowledge.ts b/packages/core/src/ragknowledge.ts index 4ccc56c8e1..5c7cdb5cd8 100644 --- a/packages/core/src/ragknowledge.ts +++ b/packages/core/src/ragknowledge.ts @@ -6,8 +6,11 @@ import { IRAGKnowledgeManager, RAGKnowledgeItem, UUID, + KnowledgeScope, } from "./types.ts"; import { stringToUuid } from "./uuid.ts"; +import { existsSync } from "fs"; +import { join } from "path"; /** * Manage knowledge in the database. @@ -23,15 +26,25 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { */ tableName: string; + /** + * The root directory where RAG knowledge files are located (internal) + */ + knowledgeRoot: string; + /** * Constructs a new KnowledgeManager instance. * @param opts Options for the manager. * @param opts.tableName The name of the table this manager will operate on. * @param opts.runtime The AgentRuntime instance associated with this manager. */ - constructor(opts: { tableName: string; runtime: IAgentRuntime }) { + constructor(opts: { + tableName: string; + runtime: IAgentRuntime; + knowledgeRoot: string; + }) { this.runtime = opts.runtime; this.tableName = opts.tableName; + this.knowledgeRoot = opts.knowledgeRoot; } private readonly defaultRAGMatchThreshold = 0.85; @@ -111,23 +124,25 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { return ""; } - return content - .replace(/```[\s\S]*?```/g, "") - .replace(/`.*?`/g, "") - .replace(/#{1,6}\s*(.*)/g, "$1") - .replace(/!\[(.*?)\]\(.*?\)/g, "$1") - .replace(/\[(.*?)\]\(.*?\)/g, "$1") - .replace(/(https?:\/\/)?(www\.)?([^\s]+\.[^\s]+)/g, "$3") - .replace(/<@[!&]?\d+>/g, "") - .replace(/<[^>]*>/g, "") - .replace(/^\s*[-*_]{3,}\s*$/gm, "") - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/\/\/.*/g, "") - .replace(/\s+/g, " ") - .replace(/\n{3,}/g, "\n\n") - .replace(/[^a-zA-Z0-9\s\-_./:?=&]/g, "") - .trim() - .toLowerCase(); + return ( + content + .replace(/```[\s\S]*?```/g, "") + .replace(/`.*?`/g, "") + .replace(/#{1,6}\s*(.*)/g, "$1") + .replace(/!\[(.*?)\]\(.*?\)/g, "$1") + .replace(/\[(.*?)\]\(.*?\)/g, "$1") + .replace(/(https?:\/\/)?(www\.)?([^\s]+\.[^\s]+)/g, "$3") + .replace(/<@[!&]?\d+>/g, "") + .replace(/<[^>]*>/g, "") + .replace(/^\s*[-*_]{3,}\s*$/gm, "") + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\/\/.*/g, "") + .replace(/\s+/g, " ") + .replace(/\n{3,}/g, "\n\n") + // .replace(/[^a-zA-Z0-9\s\-_./:?=&]/g, "") --this strips out CJK characters + .trim() + .toLowerCase() + ); } private hasProximityMatch(text: string, terms: string[]): boolean { @@ -355,6 +370,126 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { ); } + /** + * Lists all knowledge entries for an agent without semantic search or reranking. + * Used primarily for administrative tasks like cleanup. + * + * @param agentId The agent ID to fetch knowledge entries for + * @returns Array of RAGKnowledgeItem entries + */ + async listAllKnowledge(agentId: UUID): Promise { + elizaLogger.debug( + `[Knowledge List] Fetching all entries for agent: ${agentId}` + ); + + try { + // Only pass the required agentId parameter + const results = await this.runtime.databaseAdapter.getKnowledge({ + agentId: agentId, + }); + + elizaLogger.debug( + `[Knowledge List] Found ${results.length} entries` + ); + return results; + } catch (error) { + elizaLogger.error( + "[Knowledge List] Error fetching knowledge entries:", + error + ); + throw error; + } + } + + async cleanupDeletedKnowledgeFiles() { + try { + elizaLogger.debug( + "[Cleanup] Starting knowledge cleanup process, agent: ", + this.runtime.agentId + ); + + elizaLogger.debug( + `[Cleanup] Knowledge root path: ${this.knowledgeRoot}` + ); + + const existingKnowledge = await this.listAllKnowledge( + this.runtime.agentId + ); + // Only process parent documents, ignore chunks + const parentDocuments = existingKnowledge.filter( + (item) => + !item.id.includes("chunk") && item.content.metadata?.source // Must have a source path + ); + + elizaLogger.debug( + `[Cleanup] Found ${parentDocuments.length} parent documents to check` + ); + + for (const item of parentDocuments) { + const relativePath = item.content.metadata?.source; + const filePath = join(this.knowledgeRoot, relativePath); + + elizaLogger.debug( + `[Cleanup] Checking joined file path: ${filePath}` + ); + + if (!existsSync(filePath)) { + elizaLogger.warn( + `[Cleanup] File not found, starting removal process: ${filePath}` + ); + + const idToRemove = item.id; + elizaLogger.debug( + `[Cleanup] Using ID for removal: ${idToRemove}` + ); + + try { + // Just remove the parent document - this will cascade to chunks + await this.removeKnowledge(idToRemove); + + // // Clean up the cache + // const baseCacheKeyWithWildcard = `${this.generateKnowledgeCacheKeyBase( + // idToRemove, + // item.content.metadata?.isShared || false + // )}*`; + // await this.cacheManager.deleteByPattern({ + // keyPattern: baseCacheKeyWithWildcard, + // }); + + elizaLogger.success( + `[Cleanup] Successfully removed knowledge for file: ${filePath}` + ); + } catch (deleteError) { + elizaLogger.error( + `[Cleanup] Error during deletion process for ${filePath}:`, + deleteError instanceof Error + ? { + message: deleteError.message, + stack: deleteError.stack, + name: deleteError.name, + } + : deleteError + ); + } + } + } + + elizaLogger.debug("[Cleanup] Finished knowledge cleanup process"); + } catch (error) { + elizaLogger.error( + "[Cleanup] Error cleaning up deleted knowledge files:", + error + ); + } + } + + public generateScopedId(path: string, isShared: boolean): UUID { + // Prefix the path with scope before generating UUID to ensure different IDs for shared vs private + const scope = isShared ? KnowledgeScope.SHARED : KnowledgeScope.PRIVATE; + const scopedPath = `${scope}-${path}`; + return stringToUuid(scopedPath); + } + async processFile(file: { path: string; content: string; @@ -375,6 +510,12 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { `[File Progress] Starting ${file.path} (${fileSizeKB.toFixed(2)} KB)` ); + // Generate scoped ID for the file + const scopedId = this.generateScopedId( + file.path, + file.isShared || false + ); + // Step 1: Preprocessing //const preprocessStart = Date.now(); const processedContent = this.preprocess(content); @@ -390,7 +531,7 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { // Step 3: Create main document await this.runtime.databaseAdapter.createKnowledge({ - id: stringToUuid(file.path), + id: scopedId, agentId: this.runtime.agentId, content: { text: content, @@ -431,7 +572,7 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { await Promise.all( embeddings.map(async (embeddingArray, index) => { const chunkId = - `${stringToUuid(file.path)}-chunk-${i + index}` as UUID; + `${scopedId}-chunk-${i + index}` as UUID; const chunkEmbedding = new Float32Array(embeddingArray); await this.runtime.databaseAdapter.createKnowledge({ @@ -444,8 +585,9 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { type: file.type, isShared: file.isShared || false, isChunk: true, - originalId: stringToUuid(file.path), + originalId: scopedId, chunkIndex: i + index, + originalPath: file.path, }, }, embedding: chunkEmbedding, @@ -457,7 +599,7 @@ export class RAGKnowledgeManager implements IRAGKnowledgeManager { processedChunks += batch.length; const batchTime = (Date.now() - batchStart) / 1000; elizaLogger.info( - `[Batch Progress] Processed ${processedChunks}/${totalChunks} chunks (${batchTime.toFixed(2)}s for batch)` + `[Batch Progress] ${file.path}: Processed ${processedChunks}/${totalChunks} chunks (${batchTime.toFixed(2)}s for batch)` ); } diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 646dc2b0ed..80f7987747 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -37,7 +37,7 @@ import { IRAGKnowledgeManager, IVerifiableInferenceAdapter, KnowledgeItem, - //RAGKnowledgeItem, + // RAGKnowledgeItem, //Media, ModelClass, ModelProviderName, @@ -51,13 +51,25 @@ import { type Actor, type Evaluator, type Memory, + DirectoryItem, } from "./types.ts"; import { stringToUuid } from "./uuid.ts"; - +import { glob } from "glob"; +import { existsSync } from "fs"; /** * Represents the runtime environment for an agent, handling message processing, * action registration, and interaction with external services like OpenAI and Supabase. */ + +function isDirectoryItem(item: any): item is DirectoryItem { + return ( + typeof item === "object" && + item !== null && + "directory" in item && + typeof item.directory === "string" + ); +} + export class AgentRuntime implements IAgentRuntime { /** * Default count for recent messages to be kept in memory. @@ -153,6 +165,8 @@ export class AgentRuntime implements IAgentRuntime { ragKnowledgeManager: IRAGKnowledgeManager; + private readonly knowledgeRoot: string; + services: Map = new Map(); memoryManagers: Map = new Map(); cacheManager: ICacheManager; @@ -249,6 +263,22 @@ export class AgentRuntime implements IAgentRuntime { characterModelProvider: opts.character?.modelProvider, }); + elizaLogger.debug( + `[AgentRuntime] Process working directory: ${process.cwd()}` + ); + + // Define the root path once + this.knowledgeRoot = join( + process.cwd(), + "..", + "characters", + "knowledge" + ); + + elizaLogger.debug( + `[AgentRuntime] Process knowledgeRoot: ${this.knowledgeRoot}` + ); + this.#conversationLength = opts.conversationLength ?? this.#conversationLength; @@ -309,6 +339,7 @@ export class AgentRuntime implements IAgentRuntime { this.ragKnowledgeManager = new RAGKnowledgeManager({ runtime: this, tableName: "knowledge", + knowledgeRoot: this.knowledgeRoot, }); (opts.managers ?? []).forEach((manager: IMemoryManager) => { @@ -349,9 +380,9 @@ export class AgentRuntime implements IAgentRuntime { this.imageVisionModelProvider = this.character.imageVisionModelProvider ?? this.modelProvider; - elizaLogger.info("Selected model provider:", this.modelProvider); + // elizaLogger.info("Selected model provider:", this.modelProvider); duplicated log ln: 343 elizaLogger.info( - "Selected image model provider:", + "Selected image vision model provider:", this.imageVisionModelProvider ); @@ -438,17 +469,90 @@ export class AgentRuntime implements IAgentRuntime { this.character.knowledge && this.character.knowledge.length > 0 ) { + elizaLogger.info( + `[RAG Check] RAG Knowledge enabled: ${this.character.settings.ragKnowledge}` + ); + elizaLogger.info( + `[RAG Check] Knowledge items:`, + this.character.knowledge + ); + if (this.character.settings.ragKnowledge) { - await this.processCharacterRAGKnowledge( - this.character.knowledge + // Type guards with logging for each knowledge type + const [directoryKnowledge, pathKnowledge, stringKnowledge] = + this.character.knowledge.reduce( + (acc, item) => { + if (typeof item === "object") { + if (isDirectoryItem(item)) { + elizaLogger.debug( + `[RAG Filter] Found directory item: ${JSON.stringify(item)}` + ); + acc[0].push(item); + } else if ("path" in item) { + elizaLogger.debug( + `[RAG Filter] Found path item: ${JSON.stringify(item)}` + ); + acc[1].push(item); + } + } else if (typeof item === "string") { + elizaLogger.debug( + `[RAG Filter] Found string item: ${item.slice(0, 100)}...` + ); + acc[2].push(item); + } + return acc; + }, + [[], [], []] as [ + Array<{ directory: string; shared?: boolean }>, + Array<{ path: string; shared?: boolean }>, + Array, + ] + ); + + elizaLogger.info( + `[RAG Summary] Found ${directoryKnowledge.length} directories, ${pathKnowledge.length} paths, and ${stringKnowledge.length} strings` ); + + // Process each type of knowledge + if (directoryKnowledge.length > 0) { + elizaLogger.info( + `[RAG Process] Processing directory knowledge sources:` + ); + for (const dir of directoryKnowledge) { + elizaLogger.info( + ` - Directory: ${dir.directory} (shared: ${!!dir.shared})` + ); + await this.processCharacterRAGDirectory(dir); + } + } + + if (pathKnowledge.length > 0) { + elizaLogger.info( + `[RAG Process] Processing individual file knowledge sources` + ); + await this.processCharacterRAGKnowledge(pathKnowledge); + } + + if (stringKnowledge.length > 0) { + elizaLogger.info( + `[RAG Process] Processing direct string knowledge` + ); + await this.processCharacterKnowledge(stringKnowledge); + } } else { + // Non-RAG mode: only process string knowledge const stringKnowledge = this.character.knowledge.filter( (item): item is string => typeof item === "string" ); - await this.processCharacterKnowledge(stringKnowledge); } + + // After all new knowledge is processed, clean up any deleted files + elizaLogger.info( + `[RAG Cleanup] Starting cleanup of deleted knowledge files` + ); + await this.ragKnowledgeManager.cleanupDeletedKnowledgeFiles(); + elizaLogger.info(`[RAG Cleanup] Cleanup complete`); } } @@ -546,13 +650,7 @@ export class AgentRuntime implements IAgentRuntime { ["md", "txt", "pdf"].includes(fileExtension) ) { try { - const rootPath = join(process.cwd(), ".."); - const filePath = join( - rootPath, - "characters", - "knowledge", - contentItem - ); + const filePath = join(this.knowledgeRoot, contentItem); elizaLogger.info( "Attempting to read file from:", filePath @@ -667,6 +765,115 @@ export class AgentRuntime implements IAgentRuntime { } } + /** + * Processes directory-based RAG knowledge by recursively loading and processing files. + * @param dirConfig The directory configuration containing path and shared flag + */ + private async processCharacterRAGDirectory(dirConfig: { + directory: string; + shared?: boolean; + }) { + if (!dirConfig.directory) { + elizaLogger.error("[RAG Directory] No directory specified"); + return; + } + + // Sanitize directory path to prevent traversal attacks + const sanitizedDir = dirConfig.directory.replace(/\.\./g, ""); + const dirPath = join(this.knowledgeRoot, sanitizedDir); + + try { + // Check if directory exists + const dirExists = existsSync(dirPath); + if (!dirExists) { + elizaLogger.error( + `[RAG Directory] Directory does not exist: ${sanitizedDir}` + ); + return; + } + + elizaLogger.debug(`[RAG Directory] Searching in: ${dirPath}`); + // Use glob to find all matching files in directory + const files = await glob("**/*.{md,txt,pdf}", { + cwd: dirPath, + nodir: true, + absolute: false, + }); + + if (files.length === 0) { + elizaLogger.warn( + `No matching files found in directory: ${dirConfig.directory}` + ); + return; + } + + elizaLogger.info( + `[RAG Directory] Found ${files.length} files in ${dirConfig.directory}` + ); + + // Process files in batches to avoid memory issues + const BATCH_SIZE = 5; + for (let i = 0; i < files.length; i += BATCH_SIZE) { + const batch = files.slice(i, i + BATCH_SIZE); + + await Promise.all( + batch.map(async (file) => { + try { + const relativePath = join(sanitizedDir, file); + + elizaLogger.debug( + `[RAG Directory] Processing file ${i + 1}/${files.length}:`, + { + file, + relativePath, + shared: dirConfig.shared, + } + ); + + await this.processCharacterRAGKnowledge([ + { + path: relativePath, + shared: dirConfig.shared, + }, + ]); + } catch (error) { + elizaLogger.error( + `[RAG Directory] Failed to process file: ${file}`, + error instanceof Error + ? { + name: error.name, + message: error.message, + stack: error.stack, + } + : error + ); + } + }) + ); + + elizaLogger.debug( + `[RAG Directory] Completed batch ${Math.min(i + BATCH_SIZE, files.length)}/${files.length} files` + ); + } + + elizaLogger.success( + `[RAG Directory] Successfully processed directory: ${sanitizedDir}` + ); + } catch (error) { + elizaLogger.error( + `[RAG Directory] Failed to process directory: ${sanitizedDir}`, + error instanceof Error + ? { + name: error.name, + message: error.message, + stack: error.stack, + } + : error + ); + throw error; // Re-throw to let caller handle it + } + } + getSetting(key: string) { // check if the key is in the character.settings.secrets object if (this.character.settings?.secrets?.[key]) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 621ffe81c6..339c35b9bd 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -649,6 +649,7 @@ export enum Clients { AUTO = "auto", SLACK = "slack", GITHUB = "github", + INSTAGRAM = "instagram", } export interface IAgentConfig { @@ -732,6 +733,9 @@ export type Character = { twitterPostTemplate?: TemplateType; twitterMessageHandlerTemplate?: TemplateType; twitterShouldRespondTemplate?: TemplateType; + instagramPostTemplate?: TemplateType; + instagramMessageHandlerTemplate?: TemplateType; + instagramShouldRespondTemplate?: TemplateType; farcasterPostTemplate?: TemplateType; lensPostTemplate?: TemplateType; farcasterMessageHandlerTemplate?: TemplateType; @@ -869,10 +873,20 @@ export type Character = { bio: string; nicknames?: string[]; }; + + /** Optional Instagram profile */ + instagramProfile?: { + id: string; + username: string; + bio: string; + nicknames?: string[]; + }; + /** Optional NFT prompt */ nft?: { prompt: string; }; + /**Optinal Parent characters to inherit information from */ extends?: string[]; }; @@ -1139,6 +1153,7 @@ export interface IRAGKnowledgeManager { type: "pdf" | "md" | "txt"; isShared: boolean; }): Promise; + cleanupDeletedKnowledgeFiles(): Promise; } export type CacheOptions = { @@ -1558,4 +1573,23 @@ export enum TranscriptionProvider { export enum ActionTimelineType { ForYou = "foryou", Following = "following", -} \ No newline at end of file +} + +export enum KnowledgeScope { + SHARED = "shared", + PRIVATE = "private", +} + +export enum CacheKeyPrefix { + KNOWLEDGE = "knowledge", +} + +export interface DirectoryItem { + directory: string; + shared?: boolean; +} + +export interface ChunkRow { + id: string; + // Add other properties if needed +} diff --git a/packages/plugin-node/src/services/image.ts b/packages/plugin-node/src/services/image.ts index 56a59c9056..adffe10cb3 100644 --- a/packages/plugin-node/src/services/image.ts +++ b/packages/plugin-node/src/services/image.ts @@ -189,6 +189,51 @@ class OpenAIImageProvider implements ImageProvider { } } + +class GroqImageProvider implements ImageProvider { + constructor(private runtime: IAgentRuntime) {} + + async initialize(): Promise {} + + async describeImage( + imageData: Buffer, + mimeType: string + ): Promise<{ title: string; description: string }> { + const imageUrl = convertToBase64DataUrl(imageData, mimeType); + + const content = [ + { type: "text", text: IMAGE_DESCRIPTION_PROMPT }, + { type: "image_url", image_url: { url: imageUrl } }, + ]; + + const endpoint = + this.runtime.imageVisionModelProvider === ModelProviderName.GROQ + ? getEndpoint(this.runtime.imageVisionModelProvider) + : "https://api.groq.com/openai/v1/"; + + const response = await fetch(endpoint + "/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.runtime.getSetting("GROQ_API_KEY")}`, + }, + body: JSON.stringify({ + model: /*this.runtime.imageVisionModelName ||*/ "llama-3.2-90b-vision-preview", + messages: [{ role: "user", content }], + max_tokens: 1024, + }), + }); + + if (!response.ok) { + await handleApiError(response, "Groq"); + } + + const data = await response.json(); + return parseImageResponse(data.choices[0].message.content); + } +} + + class GoogleImageProvider implements ImageProvider { constructor(private runtime: IAgentRuntime) {} @@ -280,6 +325,12 @@ export class ImageDescriptionService ) { this.provider = new OpenAIImageProvider(this.runtime); elizaLogger.debug("Using openai for vision model"); + } else if ( + this.runtime.imageVisionModelProvider === + ModelProviderName.GROQ + ) { + this.provider = new GroqImageProvider(this.runtime); + elizaLogger.debug("Using Groq for vision model"); } else { elizaLogger.error( `Unsupported image vision model provider: ${this.runtime.imageVisionModelProvider}` @@ -291,6 +342,9 @@ export class ImageDescriptionService } else if (model === models[ModelProviderName.GOOGLE]) { this.provider = new GoogleImageProvider(this.runtime); elizaLogger.debug("Using google for vision model"); + } else if (model === models[ModelProviderName.GROQ]) { + this.provider = new GroqImageProvider(this.runtime); + elizaLogger.debug("Using groq for vision model"); } else { elizaLogger.debug("Using default openai for vision model"); this.provider = new OpenAIImageProvider(this.runtime); diff --git a/packages/plugin-tee/src/actions/remoteAttestation.ts b/packages/plugin-tee/src/actions/remoteAttestation.ts index 87fa15dde0..40f05b901f 100644 --- a/packages/plugin-tee/src/actions/remoteAttestation.ts +++ b/packages/plugin-tee/src/actions/remoteAttestation.ts @@ -87,15 +87,15 @@ https://proof.t16z.com/reports/${data.checksum}`, user: "{{user1}}", content: { text: "If you are running in a TEE, generate a remote attestation", + action: "REMOTE_ATTESTATION", }, }, { user: "{{user2}}", content: { text: "Of course, one second...", - action: "REMOTE_ATTESTATION", }, } ], ], -}; \ No newline at end of file +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cd62964e0..10505dfec9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,9 @@ importers: '@elizaos/client-farcaster': specifier: workspace:* version: link:../packages/client-farcaster + '@elizaos/client-instagram': + specifier: workspace:* + version: link:../packages/client-instagram '@elizaos/client-lens': specifier: workspace:* version: link:../packages/client-lens @@ -877,6 +880,31 @@ importers: specifier: 8.3.5 version: 8.3.5(@swc/core@1.10.7(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.5.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + packages/client-instagram: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + glob: + specifier: 11.0.0 + version: 11.0.0 + instagram-private-api: + specifier: ^1.45.3 + version: 1.46.1 + sharp: + specifier: ^0.33.2 + version: 0.33.5 + zod: + specifier: 3.23.8 + version: 3.23.8 + devDependencies: + '@types/sharp': + specifier: ^0.32.0 + version: 0.32.0 + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.7(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + packages/client-lens: dependencies: '@elizaos/core': @@ -6763,6 +6791,9 @@ packages: resolution: {integrity: sha512-Ku8yTGgeumayvMr8sml72EPb6WaoJhRjMTkMZrKSJtcLNDBlDpKwyUxDxNTBNBRUYWUuJCnj7eUH7pDNuc9odQ==} engines: {node: '>=18.0.0'} + '@lifeomic/attempt@3.1.0': + resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==} + '@lifi/data-types@5.15.5': resolution: {integrity: sha512-nMlXxVZTClaMNS1fty6BV7E+gyKFnOgYAIMQ1kAJLv97TdLWBwQxUVDWPI5zJKKIT/Y14PJ7H6ONx+5Gq0kRGw==} @@ -9810,6 +9841,9 @@ packages: '@types/big.js@6.2.2': resolution: {integrity: sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==} + '@types/bluebird@3.5.42': + resolution: {integrity: sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==} + '@types/bn.js@5.1.6': resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==} @@ -9822,12 +9856,18 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/chai-subset@1.3.5': resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==} '@types/chai@4.3.20': resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} + '@types/chance@1.1.6': + resolution: {integrity: sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ==} + '@types/chrome@0.0.278': resolution: {integrity: sha512-PDIJodOu7o54PpSOYLybPW/MDZBCjM1TKgf31I3Q/qaEbNpIH09rOM3tSEH3N7Q+FAqb1933LhF8ksUPYeQLNg==} @@ -10209,6 +10249,10 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/sharp@0.32.0': + resolution: {integrity: sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==} + deprecated: This is a stub types definition. sharp provides its own type definitions, so you do not need this installed. + '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} @@ -10221,6 +10265,9 @@ packages: '@types/tar@6.1.13': resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -11986,6 +12033,9 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chance@1.1.12: + resolution: {integrity: sha512-vVBIGQVnwtUG+SYe0ge+3MvF78cvSpuCOEUJr7sVEk2vSBuMW6OXNJjSzdtzrlxNUEaoqH2GBd5Y/+18BEB01Q==} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -12095,6 +12145,9 @@ packages: class-is@1.1.0: resolution: {integrity: sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==} + class-transformer@0.3.1: + resolution: {integrity: sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -15111,6 +15164,11 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + image-size@0.7.5: + resolution: {integrity: sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==} + engines: {node: '>=6.9.0'} + hasBin: true + image-size@1.2.0: resolution: {integrity: sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==} engines: {node: '>=16.x'} @@ -15210,6 +15268,15 @@ packages: resolution: {integrity: sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==} hasBin: true + instagram-private-api@1.46.1: + resolution: {integrity: sha512-fq0q6UfhpikKZ5Kw8HNwS6YpsNghE9I/uc8AM9Do9nsQ+3H1u0jLz+0t/FcGkGTjZz5VGvU8s2VbWj9wxchwYg==} + engines: {node: '>=8.0.0'} + peerDependencies: + re2: ^1.17.2 + peerDependenciesMeta: + re2: + optional: true + int64-buffer@0.1.10: resolution: {integrity: sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==} @@ -16569,6 +16636,9 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + luxon@1.28.1: + resolution: {integrity: sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==} + mafmt@7.1.0: resolution: {integrity: sha512-vpeo9S+hepT3k2h5iFxzEHvvR0GPBx9uKaErmnRzYNcaKb03DgOArjEMlgG4a9LcuZZ89a3I8xbeto487n26eA==} @@ -19556,6 +19626,9 @@ packages: reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -19656,6 +19729,19 @@ packages: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} + request-promise-core@1.1.4: + resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} + engines: {node: '>=0.10.0'} + peerDependencies: + request: ^2.34 + + request-promise@4.2.6: + resolution: {integrity: sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==} + engines: {node: '>=0.10.0'} + deprecated: request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 + peerDependencies: + request: ^2.34 + request@2.88.2: resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} engines: {node: '>= 6'} @@ -20430,6 +20516,10 @@ packages: resolution: {integrity: sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==} engines: {node: '>=16.0.0'} + stealthy-require@1.1.1: + resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} + engines: {node: '>=0.10.0'} + steno@4.0.2: resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} engines: {node: '>=18'} @@ -20964,6 +21054,10 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tlds@1.255.0: + resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==} + hasBin: true + tldts-core@6.1.71: resolution: {integrity: sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==} @@ -20992,10 +21086,19 @@ packages: resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} engines: {node: '>=0.10.0'} + to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + to-snake-case@1.0.0: + resolution: {integrity: sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==} + + to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + to-vfile@6.1.0: resolution: {integrity: sha512-BxX8EkCxOAZe+D/ToHdDsJcVI4HqQfmw0tCkp31zf3dNP/XWIAjU4CmeuSwsSoOzOTqHPOL0KUzyZqJplkD0Qw==} @@ -21098,6 +21201,11 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-custom-error@2.2.2: + resolution: {integrity: sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==} + engines: {node: '>=8.0.0'} + deprecated: npm package tarball contains useless codeclimate-reporter binary, please update to version 3.1.1. See https://github.com/adriengibrat/ts-custom-error/issues/32 + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -21149,6 +21257,9 @@ packages: '@swc/wasm': optional: true + ts-xor@1.3.0: + resolution: {integrity: sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==} + tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} @@ -21722,6 +21833,15 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url-regex-safe@3.0.0: + resolution: {integrity: sha512-+2U40NrcmtWFVjuxXVt9bGRw6c7/MgkGKN9xIfPrT/2RX0LTkkae6CCEDp93xqUN0UKm/rr821QnHd2dHQmN3A==} + engines: {node: '>= 10.12.0'} + peerDependencies: + re2: ^1.17.2 + peerDependenciesMeta: + re2: + optional: true + url-value-parser@2.2.0: resolution: {integrity: sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==} engines: {node: '>=6.0.0'} @@ -28964,6 +29084,8 @@ snapshots: - supports-color - typescript + '@lifeomic/attempt@3.1.0': {} + '@lifi/data-types@5.15.5': dependencies: '@lifi/types': 16.3.0 @@ -33824,6 +33946,8 @@ snapshots: '@types/big.js@6.2.2': {} + '@types/bluebird@3.5.42': {} + '@types/bn.js@5.1.6': dependencies: '@types/node': 20.17.9 @@ -33844,12 +33968,16 @@ snapshots: '@types/node': 20.17.9 '@types/responselike': 1.0.3 + '@types/caseless@0.12.5': {} + '@types/chai-subset@1.3.5': dependencies: '@types/chai': 4.3.20 '@types/chai@4.3.20': {} + '@types/chance@1.1.6': {} + '@types/chrome@0.0.278': dependencies: '@types/filesystem': 0.0.36 @@ -34262,6 +34390,18 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/request-promise@4.1.51': + dependencies: + '@types/bluebird': 3.5.42 + '@types/request': 2.48.12 + + '@types/request@2.48.12': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 20.17.9 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.2 + '@types/resolve@1.20.2': {} '@types/responselike@1.0.3': @@ -34293,6 +34433,10 @@ snapshots: '@types/node': 20.17.9 '@types/send': 0.17.4 + '@types/sharp@0.32.0': + dependencies: + sharp: 0.33.5 + '@types/sockjs@0.3.36': dependencies: '@types/node': 20.17.9 @@ -34309,6 +34453,8 @@ snapshots: '@types/node': 20.17.9 minipass: 4.2.8 + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': {} '@types/unist@2.0.11': {} @@ -37237,6 +37383,8 @@ snapshots: chalk@5.4.1: {} + chance@1.1.12: {} + char-regex@1.0.2: {} character-entities-html4@2.1.0: {} @@ -37367,6 +37515,8 @@ snapshots: class-is@1.1.0: {} + class-transformer@0.3.1: {} + class-transformer@0.5.1: {} class-variance-authority@0.7.1: @@ -41488,6 +41638,8 @@ snapshots: ignore@5.3.2: {} + image-size@0.7.5: {} + image-size@1.2.0: dependencies: queue: 6.0.2 @@ -41596,6 +41748,32 @@ snapshots: undeclared-identifiers: 1.1.3 xtend: 4.0.2 + instagram-private-api@1.46.1: + dependencies: + '@lifeomic/attempt': 3.1.0 + '@types/chance': 1.1.6 + '@types/request-promise': 4.1.51 + bluebird: 3.7.2 + chance: 1.1.12 + class-transformer: 0.3.1 + debug: 4.4.0(supports-color@5.5.0) + image-size: 0.7.5 + json-bigint: 1.0.0 + lodash: 4.17.21 + luxon: 1.28.1 + reflect-metadata: 0.1.14 + request: 2.88.2 + request-promise: 4.2.6(request@2.88.2) + rxjs: 6.6.7 + snakecase-keys: 3.2.1 + tough-cookie: 2.5.0 + ts-custom-error: 2.2.2 + ts-xor: 1.3.0 + url-regex-safe: 3.0.0 + utility-types: 3.11.0 + transitivePeerDependencies: + - supports-color + int64-buffer@0.1.10: {} interchain@1.10.4(bufferutil@4.0.9)(utf-8-validate@5.0.10): @@ -43520,6 +43698,8 @@ snapshots: lunr@2.3.9: {} + luxon@1.28.1: {} + mafmt@7.1.0: dependencies: multiaddr: 7.5.0 @@ -47354,6 +47534,8 @@ snapshots: reflect-metadata@0.1.13: {} + reflect-metadata@0.1.14: {} + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -47522,6 +47704,19 @@ snapshots: repeat-string@1.6.1: {} + request-promise-core@1.1.4(request@2.88.2): + dependencies: + lodash: 4.17.21 + request: 2.88.2 + + request-promise@4.2.6(request@2.88.2): + dependencies: + bluebird: 3.7.2 + request: 2.88.2 + request-promise-core: 1.1.4(request@2.88.2) + stealthy-require: 1.1.1 + tough-cookie: 2.5.0 + request@2.88.2: dependencies: aws-sign2: 0.7.0 @@ -48549,6 +48744,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 + stealthy-require@1.1.1: {} + steno@4.0.2: {} stream-browserify@3.0.0: @@ -49152,6 +49349,8 @@ snapshots: tinyspy@3.0.2: {} + tlds@1.255.0: {} + tldts-core@6.1.71: {} tldts-experimental@6.1.71: @@ -49177,10 +49376,20 @@ snapshots: to-fast-properties@1.0.3: {} + to-no-case@1.0.2: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + to-snake-case@1.0.0: + dependencies: + to-space-case: 1.0.0 + + to-space-case@1.0.0: + dependencies: + to-no-case: 1.0.2 + to-vfile@6.1.0: dependencies: is-buffer: 2.0.5 @@ -49270,6 +49479,8 @@ snapshots: dependencies: typescript: 5.6.3 + ts-custom-error@2.2.2: {} + ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} @@ -49456,6 +49667,8 @@ snapshots: optionalDependencies: '@swc/core': 1.10.7(@swc/helpers@0.5.15) + ts-xor@1.3.0: {} + tsconfck@3.1.4(typescript@5.6.3): optionalDependencies: typescript: 5.6.3 @@ -50060,6 +50273,11 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + url-regex-safe@3.0.0: + dependencies: + ip-regex: 4.3.0 + tlds: 1.255.0 + url-value-parser@2.2.0: {} url@0.11.4: