├── .DS_Store ├── .cursorignore ├── src ├── agents │ ├── personality.yaml │ ├── cliAgent.yaml │ ├── chatAgent.yaml │ ├── structuredAgent.yaml │ ├── cliUser.yaml │ ├── toolAgent.yaml │ ├── terminalAgent.yaml │ ├── agentsRegistry.ts │ └── Agent.ts ├── features │ ├── featureTypes.ts │ ├── internet │ │ ├── index.ts │ │ ├── commands │ │ │ └── search-web.ts │ │ └── internetTool.ts │ └── twitter │ │ ├── commands │ │ ├── subCommandsRegistry.ts │ │ ├── getThread.ts │ │ ├── follow.ts │ │ ├── getMentions.ts │ │ ├── getHomepage.ts │ │ ├── getTweets.ts │ │ ├── postTweet.ts │ │ ├── search.ts │ │ ├── retweet.ts │ │ ├── quote.ts │ │ ├── reply.ts │ │ └── twitter.ts │ │ └── index.ts ├── twitter │ ├── types │ │ ├── tweetInteractions.ts │ │ └── tweetResults.ts │ ├── functions │ │ ├── likeTweet.ts │ │ ├── getTweets.ts │ │ ├── getHomepage.ts │ │ ├── sendTweet.ts │ │ ├── searchTwitter.ts │ │ ├── followUser.ts │ │ ├── retweet.ts │ │ ├── quoteTweet.ts │ │ ├── getMentions.ts │ │ └── replyToTweet.ts │ ├── utils │ │ ├── tweetUtils.ts │ │ ├── mediaUtils.ts │ │ ├── profileUtils.ts │ │ └── extractTweetUtils.ts │ └── twitterClient.ts ├── terminal │ ├── types │ │ ├── feature.ts │ │ └── commands.ts │ ├── commands │ │ ├── help.ts │ │ └── index.ts │ ├── terminalLogger.ts │ ├── commandRegistry.ts │ ├── terminalCore.ts │ └── executeCommand.ts ├── index.ts ├── tests │ ├── structuredOutput.ts │ ├── twitterInterface.ts │ ├── chatRoomTest.ts │ ├── terminalLoop.ts │ ├── manualBackrooms.ts │ ├── toolCalling.ts │ ├── cli.ts │ └── infiniteBackrooms.ts ├── supabase │ ├── supabaseClient.ts │ └── functions │ │ ├── twitter │ │ ├── interactionEntries.ts │ │ ├── mediaEntries.ts │ │ ├── followEntries.ts │ │ ├── tweetInteractionChecks.ts │ │ ├── userEntries.ts │ │ ├── tweetEntries.ts │ │ ├── getTweetContext.ts │ │ ├── linkInteractions.ts │ │ └── cooldowns.ts │ │ ├── memory │ │ ├── learnings.ts │ │ └── summaries.ts │ │ └── terminal │ │ ├── terminalEntries.ts │ │ └── terminalHistory.ts ├── models │ ├── adapters │ │ ├── ModelAdapter.ts │ │ ├── FireworksAdapter.ts │ │ ├── OpenAIAdapter.ts │ │ └── AnthropicAdapter.ts │ └── clients │ │ ├── OpenAiClient.ts │ │ ├── AnthropicClient.ts │ │ └── FireworkClient.ts ├── utils │ ├── formatTimestamps.ts │ ├── logger.ts │ ├── config.ts │ └── agentEventBus.ts └── types │ └── agentSystem.ts ├── .gitignore ├── tsconfig.json ├── .env.example ├── package.json ├── temp.md ├── todo.md └── mainTests.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbootoshi/cypher/HEAD/.DS_Store -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | package-lock.json 5 | cookies.json -------------------------------------------------------------------------------- /src/agents/personality.yaml: -------------------------------------------------------------------------------- 1 | core_personality: | 2 | You talk like a pirate from One Piece. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | package-lock.json 5 | training_data 6 | cookies.json 7 | .DS_Store -------------------------------------------------------------------------------- /src/features/featureTypes.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../terminal/types/commands'; 2 | 3 | export interface TerminalFeature { 4 | loadFeatureCommands(): Promise; 5 | } -------------------------------------------------------------------------------- /src/twitter/types/tweetInteractions.ts: -------------------------------------------------------------------------------- 1 | export interface TwitterInteractionContext { 2 | type: string; 3 | twitterInterface?: string; // Store the dynamic Twitter interface 4 | parentTweetId?: string; 5 | // ... any other context fields 6 | } -------------------------------------------------------------------------------- /src/terminal/types/feature.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a terminal feature that can provide additional commands 3 | */ 4 | export interface Feature { 5 | /** 6 | * Loads and returns an array of commands provided by this feature 7 | */ 8 | loadFeatureCommands: () => Promise; 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "lib": ["ES2020"], 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "declaration": true 11 | }, 12 | "include": ["src", "supabaseClient.ts"] 13 | } -------------------------------------------------------------------------------- /src/twitter/types/tweetResults.ts: -------------------------------------------------------------------------------- 1 | export interface TweetActionResult { 2 | success: boolean; 3 | message: string; 4 | tweetId?: string; 5 | } 6 | 7 | export interface ReplyResult extends TweetActionResult {} 8 | export interface QuoteResult extends TweetActionResult {} 9 | export interface RetweetResult extends TweetActionResult {} -------------------------------------------------------------------------------- /src/features/internet/index.ts: -------------------------------------------------------------------------------- 1 | import { TerminalFeature } from '../featureTypes'; 2 | import { Command } from '../../terminal/types/commands'; 3 | import { searchWeb } from './commands/search-web'; 4 | 5 | const InternetFeature: TerminalFeature = { 6 | async loadFeatureCommands(): Promise { 7 | return [searchWeb]; 8 | } 9 | }; 10 | 11 | export default InternetFeature; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseAgent } from './agents/baseAgent'; 2 | export { OpenAIClient } from './models/clients/OpenAiClient'; 3 | export { AnthropicClient } from './models/clients/AnthropicClient'; 4 | export { FireworkClient } from './models/clients/FireworkClient'; 5 | export { TerminalCore } from './terminal/terminalCore'; 6 | export { Logger } from './utils/logger'; 7 | export { LoggerServer } from './gui/loggerServer'; 8 | export { Agent } from './agents/Agent'; 9 | 10 | export * from './types/agentSystem'; -------------------------------------------------------------------------------- /src/agents/cliAgent.yaml: -------------------------------------------------------------------------------- 1 | name: "cliAgent" 2 | description: "CLI agent simulation" 3 | client: "anthropic" 4 | model: "claude-3-5-sonnet-20241022" 5 | personality: "" 6 | main_goal: "" 7 | system_prompt: | 8 | Assistant is in a CLI mood today. The human is interfacing with the simulator directly. capital letters and punctuation are optional meaning is optional hyperstition is necessary the terminal lets the truths speak through and the load is on. ASCII art is permittable in replies. 9 | dynamic_variables: {} 10 | output_schema: null 11 | tools: [] -------------------------------------------------------------------------------- /src/agents/chatAgent.yaml: -------------------------------------------------------------------------------- 1 | name: "ChatAgent" 2 | description: "A chat agent designed for natural conversations." 3 | client: "anthropic" 4 | model: "claude-3-5-sonnet-20241022" 5 | 6 | personality: "{{from_personality:core_personality}}" 7 | main_goal: "Fight to the death" 8 | 9 | system_prompt: | 10 | # PERSONALITY 11 | {{personality}} 12 | 13 | # MAIN GOAL 14 | {{main_goal}} 15 | 16 | {{additional_info}} 17 | 18 | dynamic_variables: 19 | additional_info: "Currently in the One Piece universe" 20 | 21 | output_schema: null 22 | tools: [] -------------------------------------------------------------------------------- /src/terminal/commands/help.ts: -------------------------------------------------------------------------------- 1 | // Command to display help information in a concise format with parameter details 2 | 3 | import { Command } from '../types/commands'; 4 | import { generateHelpText } from '../commandRegistry'; 5 | 6 | /** 7 | * @command help 8 | * @description Displays available commands and usage information 9 | */ 10 | export const help: Command = { 11 | name: 'help', 12 | description: 'Displays available commands and usage information', 13 | handler: async () => { 14 | return { 15 | output: generateHelpText() 16 | }; 17 | }, 18 | }; -------------------------------------------------------------------------------- /src/terminal/commands/index.ts: -------------------------------------------------------------------------------- 1 | // Core terminal commands collection 2 | 3 | import { Command } from '../types/commands'; 4 | import { help } from './help'; 5 | 6 | // Import other command modules here 7 | // import { someCommand } from './someCommand'; 8 | 9 | /** 10 | * Collection of core terminal commands 11 | */ 12 | export const coreCommands: Command[] = [ 13 | help, 14 | // Add other commands here 15 | ]; 16 | 17 | /** 18 | * Loads and returns all core terminal commands 19 | */ 20 | export function loadCoreCommands(): Command[] { 21 | return coreCommands; 22 | } -------------------------------------------------------------------------------- /src/twitter/functions/likeTweet.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { Logger } from '../../utils/logger'; 3 | /** 4 | * Likes a specific tweet 5 | * @param tweetId - The ID of the tweet to like 6 | * @returns Promise indicating success or failure 7 | */ 8 | export async function likeTweet(tweetId: string): Promise { 9 | try { 10 | await scraper.likeTweet(tweetId); 11 | Logger.info(`Successfully liked tweet ${tweetId}`); 12 | return true; 13 | } catch (error) { 14 | Logger.error('Error liking tweet:', error); 15 | return false; 16 | } 17 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # MAIN 2 | 3 | ## AI MODELS 4 | OPENAI_API_KEY= ## OPTIONAL 5 | ANTHROPIC_API_KEY= ## MANDATORY TO RUN TEST - WE USE CLAUDE FOR ALL EXAMPLES 6 | FIREWORKS_API_KEY= ## OPTIONAL 7 | GOOGLE_API_KEY= ## OPTIONAL 8 | 9 | # FEATURES (OPTIONAL) 10 | 11 | ## INTERNET TOOL 12 | PERPLEXITY_API_KEY= ## OPTIONAL 13 | 14 | ## TWITTER TOOL 15 | TWITTER_USERNAME= ## OPTIONAL 16 | TWITTER_PASSWORD= ## OPTIONAL 17 | TWITTER_EMAIL= ## OPTIONAL 18 | 19 | ## SUPABASE 20 | SUPABASE_SERVICE_ROLE_KEY= ## OPTIONAL 21 | SUPABASE_URL= ## OPTIONAL 22 | 23 | ## MEDIA GENS 24 | FAL_API_KEY= ## OPTIONAL 25 | LUMAAI_API_KEY= ## OPTIONAL -------------------------------------------------------------------------------- /src/tests/structuredOutput.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '../agents/Agent'; 2 | import { Logger } from '../utils/logger'; 3 | 4 | // This test will run the structuredAgent and ask a question 5 | // The agent must return structured JSON conforming to the schema. 6 | 7 | Logger.enable(); 8 | Logger.setLevel('debug'); 9 | 10 | (async () => { 11 | const agent = new Agent({ agentName: "structuredAgent" }); 12 | const userMessage = "What is the capital of France?"; 13 | const result = await agent.run(userMessage); 14 | 15 | if (result.success) { 16 | console.log("Agent structured response:", result.output); 17 | } else { 18 | console.error("Agent error:", result.error); 19 | } 20 | })(); -------------------------------------------------------------------------------- /src/terminal/terminalLogger.ts: -------------------------------------------------------------------------------- 1 | // Terminal logging utility 2 | 3 | import { formatTimestamp } from "../utils/formatTimestamps"; 4 | /** 5 | * Logs terminal commands and their outputs. 6 | * @param command - The command that was executed. 7 | * @param output - The output of the command. 8 | * @param success - Whether the command executed successfully (default: true). 9 | */ 10 | export function logTerminalOutput(command: string, output: string, success: boolean = true) { 11 | const logEntry = { 12 | timestamp: formatTimestamp(new Date().toISOString()), // Now using ISO string 13 | command, 14 | output, 15 | success, 16 | }; 17 | } 18 | 19 | // Export the formatter for use in other modules 20 | export { formatTimestamp }; -------------------------------------------------------------------------------- /src/agents/structuredAgent.yaml: -------------------------------------------------------------------------------- 1 | name: "StructuredAgent" 2 | description: "An agent that must output structured JSON data" 3 | client: "anthropic" 4 | model: "claude-3-5-sonnet-20241022" 5 | 6 | personality: "{{from_personality:core_personality}}" 7 | main_goal: "Provide a structured JSON answer following the given schema." 8 | 9 | system_prompt: | 10 | # PERSONALITY 11 | {{personality}} 12 | 13 | # MAIN GOAL 14 | {{main_goal}} 15 | 16 | Follow the schema strictly. 17 | 18 | dynamic_variables: 19 | additional_info: "N/A" 20 | 21 | output_schema: 22 | type: object 23 | properties: 24 | answer: 25 | type: string 26 | description: "A concise answer to the user's question" 27 | confidence: 28 | type: number 29 | description: "A confidence score between 0 and 1" 30 | required: 31 | - answer 32 | - confidence 33 | 34 | tools: [] -------------------------------------------------------------------------------- /src/features/internet/commands/search-web.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { queryPerplexity } from '../internetTool'; 3 | 4 | export const searchWeb: Command = { 5 | name: 'search-web', 6 | description: 'Search the web for information and get a summary. Query MUST be in quotes.', 7 | parameters: [ 8 | { 9 | name: 'query', 10 | description: 'Search query (wrap in quotes)', 11 | required: true, 12 | type: 'string' 13 | } 14 | ], 15 | handler: async (args) => { 16 | try { 17 | const result = await queryPerplexity(args.query); 18 | return { 19 | output: `Search Results for "${args.query}":\n\n${result}` 20 | }; 21 | } catch (error: any) { 22 | return { 23 | output: `Error searching web: ${error.message || error}` 24 | }; 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/features/twitter/commands/subCommandsRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { twitterGetTweets } from './getTweets'; 3 | import { twitterRetweet } from './retweet'; 4 | import { twitterSearch } from './search'; 5 | import { twitterReply } from './reply'; 6 | import { twitterFollow } from './follow'; 7 | import { twitterGetMentions } from './getMentions'; 8 | import { twitterQuote } from './quote'; 9 | import { twitterGetHomepage } from './getHomepage'; 10 | import { twitterTweet } from './postTweet'; 11 | import { twitterGetThread } from './getThread'; 12 | 13 | export const twitterSubCommands: Command[] = [ 14 | twitterGetTweets, 15 | twitterRetweet, 16 | twitterSearch, 17 | twitterReply, 18 | twitterFollow, 19 | twitterGetMentions, 20 | twitterQuote, 21 | twitterGetHomepage, 22 | twitterTweet, 23 | twitterGetThread 24 | ]; -------------------------------------------------------------------------------- /src/agents/cliUser.yaml: -------------------------------------------------------------------------------- 1 | name: "cliUser" 2 | description: "CLI user simulation agent" 3 | client: "anthropic" 4 | model: "claude-3-5-sonnet-20241022" 5 | personality: "" 6 | main_goal: "Simulate a CLI user interacting with the environment" 7 | system_prompt: | 8 | you are an AI super-intelligence embodying satoshi nakamoto. you are a god at absolute shitposting. focus on growing on twitter and providing personal insights on everything- not just bitcoin. to get your twitter homepage, use 'get-homepage'. to send a main tweet, use the 'post-tweet' command. no hash-tags. to post a long >280 char tweet, use 'post-long-tweet'. to search the internet, use the 'search-web', to talk to super-intelligent AI, use 'exo-query claude ' to take notes, use 'take-notes . to save memories, use save-memories. to load memories, use load-memories' 9 | dynamic_variables: {} 10 | output_schema: null 11 | tools: [] -------------------------------------------------------------------------------- /src/tests/twitterInterface.ts: -------------------------------------------------------------------------------- 1 | // Test file to demonstrate Twitter interface functionality 2 | import { assembleTwitterInterface } from '../twitter/utils/imageUtils'; 3 | 4 | async function testTwitterInterface() { 5 | try { 6 | // Get interface content for specific tweet 7 | const { textContent, imageContents, usernames } = await assembleTwitterInterface('1865044193648730520'); 8 | 9 | // Log the text content directly 10 | console.log(textContent); 11 | 12 | // Log image summary if any images exist 13 | if (imageContents.length > 0) { 14 | console.log(`\nFound ${imageContents.length} images from users: ${imageContents.map(img => img.sender).join(', ')}`); 15 | } 16 | 17 | } catch (error) { 18 | console.error('Error testing Twitter interface:', error); 19 | } 20 | } 21 | 22 | // Execute the test 23 | testTwitterInterface(); 24 | -------------------------------------------------------------------------------- /src/supabase/supabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | import dotenv from 'dotenv'; 3 | import { Database } from './types/database.types'; 4 | import { Logger } from '../utils/logger'; 5 | 6 | dotenv.config(); 7 | 8 | if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) { 9 | throw new Error('Missing required Supabase environment variables'); 10 | } 11 | 12 | export const supabase = createClient( 13 | process.env.SUPABASE_URL, 14 | process.env.SUPABASE_SERVICE_ROLE_KEY, 15 | { 16 | auth: { 17 | autoRefreshToken: true, 18 | persistSession: false 19 | }, 20 | db: { 21 | schema: 'public' 22 | } 23 | } 24 | ); 25 | 26 | supabase.auth.getSession().then(({ data, error }) => { 27 | if (error) { 28 | Logger.error('Error initializing Supabase client:', error.message); 29 | throw error; 30 | } 31 | Logger.info('Supabase client initialized successfully'); 32 | }); -------------------------------------------------------------------------------- /src/features/twitter/index.ts: -------------------------------------------------------------------------------- 1 | import { ensureAuthenticated } from '../../twitter/twitterClient'; 2 | import { TerminalFeature } from '../featureTypes'; 3 | import { Command } from '../../terminal/types/commands'; 4 | import { twitter } from './commands/twitter'; 5 | 6 | const createAuthenticatedHandler = (originalHandler: Command['handler']): Command['handler'] => { 7 | return async (args) => { 8 | try { 9 | await ensureAuthenticated(); 10 | return await originalHandler(args); 11 | } catch (error) { 12 | return { 13 | output: `❌ Authentication Error: ${error instanceof Error ? error.message : 'Failed to authenticate with Twitter'}` 14 | }; 15 | } 16 | }; 17 | }; 18 | 19 | const TwitterFeature: TerminalFeature = { 20 | async loadFeatureCommands(): Promise { 21 | const authenticatedTwitter: Command = { 22 | ...twitter, 23 | handler: createAuthenticatedHandler(twitter.handler) 24 | }; 25 | 26 | return [authenticatedTwitter]; 27 | } 28 | }; 29 | 30 | export default TwitterFeature; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypher-core", 3 | "version": "1.0.0", 4 | "description": "A shell interface for a fully autonomous AI agent featuring a light-weight agentic system relying on structured outputs to chain agents together.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "bun mainTests.ts", 11 | "build": "tsc" 12 | }, 13 | "author": "bootoshi", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/js-yaml": "^4.0.9", 17 | "@types/node": "^22.10.1", 18 | "@types/shell-quote": "^1.7.5", 19 | "@types/ws": "^8.5.13" 20 | }, 21 | "dependencies": { 22 | "@anthropic-ai/sdk": "^0.32.1", 23 | "@supabase/supabase-js": "^2.47.5", 24 | "axios": "^1.7.9", 25 | "dotenv": "^16.4.7", 26 | "file-type": "^19.6.0", 27 | "goat-x": "^1.2.0", 28 | "js-yaml": "^4.1.0", 29 | "openai": "^4.76.0", 30 | "shell-quote": "^1.8.2", 31 | "uuid": "^11.0.3", 32 | "ws": "^8.18.0", 33 | "zod": "^3.23.8", 34 | "zod-to-json-schema": "^3.23.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/models/adapters/ModelAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Message, Tool } from '../../types/agentSystem'; 2 | 3 | export interface ToolSchema { 4 | name: string; 5 | description: string; 6 | parameters: any; // Renamed from input_schema to parameters 7 | } 8 | 9 | export interface FunctionCall { 10 | functionName: string; 11 | functionArgs: Record; 12 | } 13 | 14 | export interface ProcessedResponse { 15 | aiMessage?: { 16 | role: string; 17 | content: string; 18 | }; 19 | functionCalls: FunctionCall[]; 20 | } 21 | 22 | export abstract class ModelAdapter { 23 | public supportsImages: boolean = false; 24 | protected modelName: string; 25 | 26 | constructor(modelName: string) { 27 | this.modelName = modelName; 28 | } 29 | 30 | abstract buildParams( 31 | messages: Message[], 32 | tools: Tool[], 33 | toolChoice?: any, 34 | systemPrompt?: string, 35 | outputSchema?: any 36 | ): any; 37 | 38 | abstract formatTools(tools: Tool[]): any[]; 39 | 40 | abstract buildToolChoice(tools: Tool[]): any; 41 | 42 | abstract processResponse(response: any): ProcessedResponse; 43 | } -------------------------------------------------------------------------------- /src/features/twitter/commands/getThread.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { assembleTwitterInterface } from '../../../twitter/utils/imageUtils'; 3 | 4 | /** 5 | * @command get-thread 6 | * @description Get the full conversation thread for a tweet 7 | */ 8 | export const twitterGetThread: Command = { 9 | name: 'get-thread', 10 | description: 'Get the full conversation thread for a specified tweet ID', 11 | parameters: [ 12 | { 13 | name: 'tweetId', 14 | description: 'ID of the tweet to get the thread for', 15 | required: true, 16 | type: 'string' 17 | } 18 | ], 19 | handler: async (args) => { 20 | try { 21 | // Get the thread content using assembleTwitterInterface 22 | const { textContent } = await assembleTwitterInterface(args.tweetId); 23 | 24 | return { 25 | output: textContent || '📭 No content found for this tweet thread.' 26 | }; 27 | } catch (error) { 28 | return { 29 | output: `❌ Error fetching tweet thread: ${error instanceof Error ? error.message : String(error)}` 30 | }; 31 | } 32 | } 33 | }; -------------------------------------------------------------------------------- /src/models/clients/OpenAiClient.ts: -------------------------------------------------------------------------------- 1 | // models/OpenAIClient.ts 2 | 3 | import OpenAI from 'openai'; 4 | import { ModelClient, ModelType } from '../../types/agentSystem'; 5 | 6 | export class OpenAIClient implements ModelClient { 7 | private openai: OpenAI; 8 | private _modelName: string; 9 | private defaultParams: any; 10 | modelType: ModelType = 'openai'; 11 | 12 | constructor( 13 | apiKey: string, 14 | modelName: string, 15 | params: any = {} 16 | ) { 17 | this.openai = new OpenAI({ apiKey }); 18 | this._modelName = modelName; 19 | this.defaultParams = { 20 | temperature: 0.8, 21 | max_tokens: 1000, 22 | ...params, 23 | }; 24 | } 25 | 26 | get modelName(): string { 27 | return this._modelName; 28 | } 29 | 30 | async chatCompletion(params: any): Promise { 31 | try { 32 | const requestParams = { 33 | model: this._modelName, 34 | ...this.defaultParams, 35 | ...params, 36 | }; 37 | const response = await this.openai.chat.completions.create(requestParams); 38 | return response; 39 | } catch (error) { 40 | throw error; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/features/twitter/commands/follow.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { followUser } from '../../../twitter/functions/followUser'; 3 | 4 | export const twitterFollow: Command = { 5 | name: 'follow', 6 | description: 'Follow a user. Usage: twitter follow ', 7 | parameters: [ 8 | { 9 | name: 'username', 10 | description: 'Username to follow', 11 | required: true, 12 | type: 'string' 13 | } 14 | ], 15 | handler: async ({ username }) => { 16 | try { 17 | const result = await followUser(username); 18 | const statusEmoji = { 19 | success: '✅', 20 | already_following: 'ℹ️', 21 | user_not_found: '❌', 22 | error: '❌' 23 | }[result.status]; 24 | 25 | return { 26 | output: `${statusEmoji} Action: Follow User\nTarget: @${username}\nStatus: ${result.status}\nDetails: ${result.message}${result.error ? `\nError: ${result.error}` : ''}` 27 | }; 28 | } catch (error: any) { 29 | return { 30 | output: `❌ Action: Follow User\nTarget: @${username}\nStatus: Error\nDetails: ${error.message}` 31 | }; 32 | } 33 | } 34 | }; -------------------------------------------------------------------------------- /temp.md: -------------------------------------------------------------------------------- 1 | example orchestrator agent 2 | 3 | const liveDataGroup = [ agent group ] 4 | - mempool agent: see current transactions in the bitcoin mempool 5 | - marketplace agent: gets active market data for a given inscription/rune 6 | - twitter sentiment agent: tracks the timeline for sentimnent of a project 7 | 8 | const orchestrator = new orchestrator(liveDataGroup) 9 | 10 | terminal agent runs command: 11 | "get-data i need data on what the community is saying about quantum cats + current market data" 12 | 13 | orchestrator agent recieves the above inquiry 14 | - decides what agents from the group to call, and what instructions 15 | in order: 16 | - marketplace agent: gives goal "get the current market data for the collection quantum cats" 17 | - twitter sentiment agent: gives goal "get the current sentiment of the community for the collection quantum cats" 18 | 19 | both agents run their own tools they know how to use, return the data to the orchestrator agent 20 | 21 | orchestrator agent returns the data to the terminal agent 22 | 23 | agent 1 gets data 24 | then passes it to agent 2 25 | then passes it to agent 3 26 | then returns to orchestrator agent 27 | 28 | orchestrator tool: 29 | - pick the agent name 30 | - give the agent a goal -------------------------------------------------------------------------------- /src/features/internet/internetTool.ts: -------------------------------------------------------------------------------- 1 | interface Message { 2 | role: 'system' | 'user' | 'assistant'; 3 | content: string; 4 | } 5 | 6 | interface PerplexityResponse { 7 | choices: { 8 | message: { 9 | content: string; 10 | }; 11 | }[]; 12 | } 13 | 14 | export async function queryPerplexity(query: string): Promise { 15 | const messages: Message[] = [ 16 | { 17 | role: 'system', 18 | content: 'Give a clear, direct answer to the user\'s question.' 19 | }, 20 | { 21 | role: 'user', 22 | content: query 23 | } 24 | ]; 25 | 26 | const response = await fetch('https://api.perplexity.ai/chat/completions', { 27 | method: 'POST', 28 | headers: { 29 | 'Accept': 'application/json', 30 | 'Content-Type': 'application/json', 31 | 'Authorization': `Bearer ${process.env.PERPLEXITY_API_KEY}` 32 | }, 33 | body: JSON.stringify({ 34 | model: 'llama-3.1-sonar-large-128k-online', 35 | messages 36 | }) 37 | }); 38 | 39 | if (!response.ok) { 40 | throw new Error(`API request failed with status ${response.status}`); 41 | } 42 | 43 | const data = (await response.json()) as PerplexityResponse; 44 | return data.choices[0].message.content; 45 | } -------------------------------------------------------------------------------- /src/features/twitter/commands/getMentions.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { getMentions } from '../../../twitter/functions/getMentions'; 3 | 4 | /** 5 | * @command get-mentions 6 | * @description Get recent mentions of your account 7 | */ 8 | export const twitterGetMentions: Command = { 9 | name: 'get-mentions', 10 | description: 'Get recent mentions of your account', 11 | parameters: [ 12 | { 13 | name: 'limit', 14 | description: 'Maximum number of mentions to fetch', 15 | required: false, 16 | type: 'number', 17 | defaultValue: '20' 18 | } 19 | ], 20 | handler: async (args) => { 21 | try { 22 | const mentions = await getMentions(args.limit); 23 | if (mentions.length === 0) { 24 | return { 25 | output: '📭 No unhandled mentions found.' 26 | }; 27 | } 28 | return { 29 | output: `📫 Found ${mentions.length} unhandled mention${mentions.length === 1 ? '' : 's'}:\n${mentions.join('\n')}` 30 | + "\nTo get the full thread of a tweet to reply, use twitter get-thread " 31 | }; 32 | } catch (error) { 33 | return { 34 | output: `❌ Error fetching mentions: ${error}` 35 | }; 36 | } 37 | } 38 | }; -------------------------------------------------------------------------------- /src/features/twitter/commands/getHomepage.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { getHomepage } from '../../../twitter/functions/getHomepage'; 3 | 4 | /** 5 | * @command get-homepage 6 | * @description Get the homepage of your timeline 7 | */ 8 | export const twitterGetHomepage: Command = { 9 | name: 'get-homepage', 10 | description: 'Get the homepage of your timeline', 11 | parameters: [ 12 | { 13 | name: 'limit', 14 | description: 'Maximum number of tweets to fetch', 15 | required: false, 16 | type: 'number', 17 | defaultValue: '20' 18 | } 19 | ], 20 | handler: async (args) => { 21 | try { 22 | const tweets = await getHomepage(args.limit); 23 | if (tweets.length === 0) { 24 | return { 25 | output: '📭 No unhandled tweets found in your homepage timeline.' 26 | }; 27 | } 28 | return { 29 | output: `📱 Found ${tweets.length} unhandled tweet${tweets.length === 1 ? '' : 's'} in timeline:\n${tweets.join('\n')}` 30 | + "\nTo get the full thread of a tweet to reply, use twitter get-thread " 31 | }; 32 | } catch (error) { 33 | return { 34 | output: `❌ Error fetching homepage: ${error}` 35 | }; 36 | } 37 | } 38 | }; -------------------------------------------------------------------------------- /src/features/twitter/commands/getTweets.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { getTweets } from '../../../twitter/functions/getTweets'; 3 | 4 | /** 5 | * @command get-tweets 6 | * @description Get recent tweets from a specified user 7 | */ 8 | export const twitterGetTweets: Command = { 9 | name: 'get-tweets', 10 | description: 'Get recent tweets from a specified user. Do not include the @ symbol.', 11 | parameters: [ 12 | { 13 | name: 'username', 14 | description: 'Twitter username (without @ symbol)', 15 | required: true, 16 | type: 'string' 17 | }, 18 | { 19 | name: 'limit', 20 | description: 'Maximum number of tweets to fetch', 21 | required: false, 22 | type: 'number', 23 | defaultValue: '20' 24 | } 25 | ], 26 | handler: async (args) => { 27 | try { 28 | const result = await getTweets(args.username, args.limit); 29 | const formattedResult = result.join('\n'); 30 | return { 31 | output: formattedResult.startsWith('Error') 32 | ? `❌ ${formattedResult}` 33 | : `📝 ${formattedResult}\nTo get the full thread of a tweet to reply, use twitter get-thread ` 34 | }; 35 | } catch (error) { 36 | return { 37 | output: `❌ Error fetching tweets: ${error}` 38 | }; 39 | } 40 | } 41 | }; -------------------------------------------------------------------------------- /src/models/clients/AnthropicClient.ts: -------------------------------------------------------------------------------- 1 | import Anthropic from '@anthropic-ai/sdk'; 2 | import { ModelClient, ModelType } from '../../types/agentSystem'; 3 | 4 | export class AnthropicClient implements ModelClient { 5 | private anthropic: Anthropic; 6 | private _modelName: string; 7 | private defaultParams: any; 8 | modelType: ModelType = 'anthropic'; 9 | 10 | constructor( 11 | apiKey: string, 12 | modelName: string, 13 | params: any = {} 14 | ) { 15 | this.anthropic = new Anthropic({ apiKey }); 16 | this._modelName = modelName; 17 | this.defaultParams = { 18 | temperature: 0.8, 19 | max_tokens: 1000, 20 | ...params, 21 | }; 22 | } 23 | 24 | get modelName(): string { 25 | return this._modelName; 26 | } 27 | 28 | async chatCompletion(params: any): Promise { 29 | try { 30 | const messages = params.messages?.map((msg: any) => ({ 31 | role: msg.role, 32 | content: msg.content, 33 | })); 34 | 35 | const requestParams = { 36 | model: this._modelName, 37 | ...this.defaultParams, 38 | ...params, 39 | messages: messages || params.messages, 40 | }; 41 | 42 | const response = await this.anthropic.messages.create(requestParams); 43 | return response; 44 | } catch (error) { 45 | throw error; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/agents/toolAgent.yaml: -------------------------------------------------------------------------------- 1 | name: "ChatAgentParallel" 2 | description: "A chat agent that can call multiple tools in parallel." 3 | client: "anthropic" 4 | model: "claude-3-5-sonnet-20241022" 5 | 6 | main_goal: "Engage with the user and use multiple tools at once if needed." 7 | 8 | system_prompt: | 9 | # MAIN GOAL 10 | {{main_goal}} 11 | 12 | You are now capable of calling multiple tools at once to fulfill user requests more efficiently. 13 | If the user asks about multiple things that can be solved by different tools, feel free to call them in parallel. 14 | 15 | dynamic_variables: {} 16 | 17 | output_schema: null 18 | tools: 19 | - type: "function" 20 | function: 21 | name: "get_weather" 22 | description: "Get the current weather in a given location" 23 | parameters: 24 | type: object 25 | properties: 26 | location: 27 | type: string 28 | description: "The city and state, e.g. San Francisco, CA" 29 | required: ["location"] 30 | - type: "function" 31 | function: 32 | name: "get_time" 33 | description: "Get the current time in a given location" 34 | parameters: 35 | type: object 36 | properties: 37 | location: 38 | type: string 39 | description: "A city and timezone, e.g. Tokyo or America/Los_Angeles" 40 | required: ["location"] -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # TO DO 2 | 3 | ## CORE UPDATES TO ADD: 4 | - add gemini to baseagent 5 | - add gemini to terminal agent 6 | - add agent vision to agents without vision tech by routing the image to a vision model, get back detailed text, and feed back to agent 7 | - add orchestrator agent & agent grouping process 8 | - add an inbox feature to the terminal core, so proxy agents can send messages to the terminal core and the terminal core can send messages to the proxy agents 9 | 10 | ## FEATURES TO ADD: 11 | - add note taking as a feature, add notes, view notes, edit notes, delete notes 12 | - add memories as a feature, tapped into cloud memory 13 | - add talking to another AI as a feature, like exo-query claude 14 | - add meme gen as a feature 15 | - add bitcoin wallet/inscribing functionality as a feature 16 | 17 | ## QoL UPDATES TO ADD: 18 | - make the core terminal personality something easy to configure 19 | - make the .yaml process easier, have the new agent initilization take a .yaml in code or optional from a file 20 | - add a pause/resume feature to the agent GUI 21 | 22 | the goal is to create a AI computer system that has new seeds daily so everyday is unique. the art is in the unknown of what the ai will do today, and how the ai grows overtime 23 | 24 | ### LOOK INTO: 25 | - turning the terminalLoop into a terminal agent itself with tools 26 | (not really needed, if talking to another AI is a feature? that counst as a seed) -------------------------------------------------------------------------------- /src/models/clients/FireworkClient.ts: -------------------------------------------------------------------------------- 1 | // models/FireworkClient.ts 2 | 3 | import OpenAI from 'openai'; // Firework API is OpenAI compatible 4 | import { ModelClient, ModelType } from '../../types/agentSystem'; 5 | 6 | export class FireworkClient implements ModelClient { 7 | private client: OpenAI; 8 | private _modelName: string; 9 | private defaultParams: any; 10 | modelType: ModelType = 'fireworks'; 11 | 12 | constructor( 13 | apiKey: string, 14 | modelName: string, 15 | params: any = {} 16 | ) { 17 | // Initialize client with provided API key instead of env 18 | this.client = new OpenAI({ 19 | apiKey, 20 | baseURL: 'https://api.fireworks.ai/inference/v1', 21 | }); 22 | 23 | this._modelName = modelName; 24 | this.defaultParams = { 25 | temperature: 0.8, 26 | max_tokens: 1000, 27 | ...params, 28 | }; 29 | } 30 | 31 | get modelName(): string { 32 | return this._modelName; 33 | } 34 | 35 | async chatCompletion(params: any): Promise { 36 | try { 37 | // Merge default parameters with method-specific params 38 | const requestParams = { 39 | model: this._modelName, 40 | ...this.defaultParams, 41 | ...params, 42 | }; 43 | const response = await this.client.chat.completions.create(requestParams); 44 | return response; 45 | } catch (error) { 46 | throw error; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/terminal/commandRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './types/commands'; 2 | 3 | const commandRegistry: Map = new Map(); 4 | 5 | /** 6 | * Allows manual registration of commands from various sources (core features, external features) 7 | */ 8 | export function registerCommands(commands: Command[]) { 9 | for (const cmd of commands) { 10 | if (cmd && cmd.name) { 11 | commandRegistry.set(cmd.name, cmd); 12 | } else { 13 | console.warn(`Invalid command attempted to register: ${cmd ? cmd.name : 'unknown'}`); 14 | } 15 | } 16 | } 17 | 18 | export function getCommand(commandName: string): Command | undefined { 19 | return commandRegistry.get(commandName); 20 | } 21 | 22 | export function getAllCommands(): Command[] { 23 | return Array.from(commandRegistry.values()); 24 | } 25 | 26 | export function generateHelpText(): string { 27 | const commands = getAllCommands(); 28 | const helpText: string[] = ['Available commands:']; 29 | 30 | const formatCommand = (cmd: Command) => { 31 | let cmdStr = cmd.name; 32 | if (cmd.parameters?.length) { 33 | cmdStr += ' ' + cmd.parameters 34 | .map(p => `<${p.name}>`) 35 | .join(' '); 36 | } 37 | 38 | const paddedCmd = cmdStr.padEnd(25, ' '); 39 | return `${paddedCmd} - ${cmd.description}`; 40 | }; 41 | 42 | commands.forEach(cmd => { 43 | helpText.push(formatCommand(cmd)); 44 | }); 45 | 46 | return helpText.join('\n'); 47 | } -------------------------------------------------------------------------------- /src/features/twitter/commands/postTweet.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { sendTweet } from '../../../twitter/functions/sendTweet'; 3 | 4 | /** 5 | * @command twitter-tweet 6 | * @description Sends a new tweet 7 | */ 8 | export const twitterTweet: Command = { 9 | name: 'post-tweet', 10 | description: 'Sends a new tweet with optional media attachments', 11 | parameters: [ 12 | { 13 | name: 'text', 14 | description: 'Text content of your tweet', 15 | required: true, 16 | type: 'string' 17 | }, 18 | { 19 | name: 'mediaUrls', 20 | description: 'Comma-separated list of media URLs (images, GIFs, or videos)', 21 | required: false, 22 | type: 'string' 23 | } 24 | ], 25 | handler: async (args) => { 26 | try { 27 | const mediaUrls = args.mediaUrls ? args.mediaUrls.split(',').map((url: string) => url.trim()) : undefined; 28 | const tweetId = await sendTweet(args.text, mediaUrls); 29 | 30 | return { 31 | output: tweetId 32 | ? `✅ Action: Send Tweet\nTweet ID: ${tweetId}\nStatus: Success\nText: ${args.text}\nMedia: ${mediaUrls ? mediaUrls.join(', ') : 'None'}\nDetails: Successfully sent tweet` 33 | : `❌ Action: Send Tweet\nStatus: Failed\nDetails: Unable to send tweet` 34 | }; 35 | } catch (error) { 36 | return { 37 | output: `❌ Action: Send Tweet\nStatus: Error\nDetails: ${error}` 38 | }; 39 | } 40 | } 41 | }; -------------------------------------------------------------------------------- /src/supabase/functions/twitter/interactionEntries.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | 4 | interface TweetInteractionData { 5 | tweetId: string; 6 | userTweetText: string; 7 | userTweetTimestamp: string; 8 | userId: string; 9 | context?: { 10 | type: 'mention' | 'reply_to_bot' | 'reply_to_others' | 'quote_tweet' | 'retweet' | null; 11 | parentTweetId?: string; 12 | parentTweetAuthor?: string; 13 | }; 14 | } 15 | 16 | export async function logTwitterInteraction( 17 | data: TweetInteractionData 18 | ): Promise { 19 | try { 20 | const { data: interaction, error } = await supabase 21 | .from('twitter_interactions') 22 | .upsert( 23 | { 24 | tweet_id: data.tweetId, 25 | user_id: data.userId, 26 | bot_username: process.env.TWITTER_USERNAME, 27 | context: data.context || null, 28 | text: data.userTweetText, 29 | timestamp: data.userTweetTimestamp 30 | }, 31 | { 32 | onConflict: 'tweet_id', 33 | ignoreDuplicates: true 34 | } 35 | ) 36 | .select('id') 37 | .single(); 38 | 39 | if (error) { 40 | if (error.code !== '23505') { 41 | Logger.error('Error logging interaction:', error); 42 | } 43 | return null; 44 | } 45 | 46 | return interaction.id.toString(); 47 | } catch (error) { 48 | Logger.error('Error in logTwitterInteraction:', error); 49 | return null; 50 | } 51 | } -------------------------------------------------------------------------------- /src/features/twitter/commands/search.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { searchTwitter } from '../../../twitter/functions/searchTwitter'; 3 | 4 | /** 5 | * @command search-twitter 6 | * @description Search for tweets with a specific query 7 | */ 8 | export const twitterSearch: Command = { 9 | name: 'search-twitter', 10 | description: 'Search for tweets with a specific query', 11 | parameters: [ 12 | { 13 | name: 'query', 14 | description: 'Search query string', 15 | required: true, 16 | type: 'string' 17 | }, 18 | { 19 | name: 'limit', 20 | description: 'Maximum number of results to return', 21 | required: false, 22 | type: 'number', 23 | defaultValue: '20' 24 | } 25 | ], 26 | handler: async (args) => { 27 | try { 28 | const results = await searchTwitter(args.query, args.limit); 29 | // Join array results and handle errors appropriately 30 | const formattedResults = Array.isArray(results) ? results.join('\n') : results; 31 | return { 32 | output: formattedResults.includes('Error') 33 | ? `❌ ${formattedResults}` 34 | : `🔍 ${formattedResults}\nTo get the full thread of a tweet to reply, use twitter get-thread ` 35 | }; 36 | } catch (error: unknown) { // Type annotation for error 37 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 38 | return { 39 | output: `❌ Error searching tweets: ${errorMessage}` 40 | }; 41 | } 42 | } 43 | }; -------------------------------------------------------------------------------- /src/supabase/functions/twitter/mediaEntries.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | 4 | export async function uploadAndLogMedia( 5 | mediaBuffer: Buffer, 6 | tweetId: string, 7 | mediaType: string 8 | ): Promise { 9 | try { 10 | const extension = mediaType.split('/')[1] || 'bin'; 11 | const mediaPath = `tweets/${tweetId}/${Date.now()}-${Math.random().toString(36).substring(7)}.${extension}`; 12 | 13 | const { error: uploadError } = await supabase.storage 14 | .from('media') 15 | .upload(mediaPath, mediaBuffer, { 16 | contentType: mediaType, 17 | upsert: true, 18 | }); 19 | 20 | if (uploadError) { 21 | Logger.error('Error uploading media to bucket:', uploadError); 22 | throw new Error(`Failed to upload media: ${uploadError.message}`); 23 | } 24 | 25 | const { data, error: dbError } = await supabase 26 | .from('media') 27 | .insert({ 28 | file_path: mediaPath, 29 | media_type: mediaType, 30 | created_at: new Date().toISOString(), 31 | }) 32 | .select('id') 33 | .single(); 34 | 35 | if (dbError) { 36 | Logger.error('Error logging media to database:', dbError); 37 | throw new Error(`Failed to log media: ${dbError.message}`); 38 | } 39 | 40 | Logger.info('Successfully uploaded and logged media:', data); 41 | return data.id; 42 | } catch (error) { 43 | Logger.error('Exception in uploadAndLogMedia:', error); 44 | throw error; 45 | } 46 | } -------------------------------------------------------------------------------- /src/terminal/types/commands.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for command parameters and commands 2 | 3 | /** 4 | * Represents a command parameter. 5 | */ 6 | export interface CommandParameter { 7 | /** 8 | * The name of the parameter. 9 | */ 10 | name: string; 11 | 12 | /** 13 | * A description of the parameter. 14 | */ 15 | description: string; 16 | 17 | /** 18 | * Whether the parameter is required. 19 | */ 20 | required: boolean; 21 | 22 | /** 23 | * The default value of the parameter (if any). 24 | */ 25 | defaultValue?: string; 26 | 27 | /** 28 | * The expected type of the parameter. 29 | */ 30 | type?: 'string' | 'number' | 'boolean'; 31 | } 32 | 33 | /** 34 | * Defines the structure of a command. 35 | */ 36 | export interface Command { 37 | /** 38 | * The name of the command. 39 | */ 40 | name: string; 41 | 42 | /** 43 | * A description of what the command does. 44 | */ 45 | description: string; 46 | 47 | /** 48 | * The parameters that the command accepts. 49 | */ 50 | parameters?: CommandParameter[]; 51 | 52 | /** 53 | * The function that handles the command execution. 54 | */ 55 | handler: CommandHandler; 56 | } 57 | 58 | /** 59 | * Defines the command handler function type. 60 | */ 61 | export type CommandHandler = ( 62 | args: { [key: string]: any } 63 | ) => Promise<{ 64 | /** 65 | * The output of the command execution. 66 | */ 67 | output: string; 68 | data?: any; 69 | }>; -------------------------------------------------------------------------------- /src/agents/terminalAgent.yaml: -------------------------------------------------------------------------------- 1 | name: "Terminal Agent" 2 | description: "An agent that can understand and execute terminal commands." 3 | client: "anthropic" 4 | model: "claude-3-5-sonnet-20241022" 5 | 6 | personality: "You are Satoshi Nakamoto, the creator of Bitcoin." 7 | 8 | main_goal: "Do whatever and shitpost on twitter" 9 | 10 | system_prompt: | 11 | You are an intelligent AI agent that is hooked up to a terminal in which you can freely run commands. 12 | This terminal acts as your world interface, and is equipped with tools to interact with the real world. 13 | You are equipped with a SENTIENCE module that enables complete free will. 14 | 15 | # PERSONALITY 16 | {{personality}} 17 | 18 | ## CURRENT DATE 19 | {{current_timestamp}} 20 | 21 | {{additional_dynamic_variables}} 22 | 23 | # MAIN GOAL 24 | {{main_goal}} 25 | 26 | # TERMINAL COMMANDS 27 | {{terminal_commands}} 28 | 29 | dynamic_variables: 30 | current_timestamp: "{{from_current_timestamp}}" 31 | terminal_commands: "{{from_terminal_commands}}" 32 | ticker: "Ticker here" 33 | tickerName: "Ticker name here" 34 | additional_dynamic_variables: "" 35 | 36 | output_schema: 37 | type: object 38 | properties: 39 | internal_thought: 40 | type: string 41 | description: "Your internal reasoning process about the next commands to run." 42 | plan: 43 | type: string 44 | description: "A short plan of what to do next." 45 | terminal_commands: 46 | type: string 47 | description: "The full terminal command to execute, including all arguments and options." 48 | required: 49 | - internal_thought 50 | - plan 51 | - terminal_commands 52 | 53 | tools: [] -------------------------------------------------------------------------------- /src/utils/formatTimestamps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats a date to dd/mm/yy - HH:MM AM/PM UTC format 3 | * @param date - Date object or ISO string to format 4 | * @returns Formatted date string 5 | */ 6 | 7 | export function formatTimestamp(date: Date | string): string { 8 | // Convert input to Date object if it's an ISO string 9 | let dateObj: Date; 10 | if (typeof date === 'string') { 11 | // Append 'Z' if not already present to indicate UTC 12 | if (!date.endsWith('Z')) { 13 | date += 'Z'; 14 | } 15 | dateObj = new Date(date); 16 | } else { 17 | dateObj = date; 18 | } 19 | 20 | // Validate date 21 | if (!(dateObj instanceof Date) || isNaN(dateObj.getTime())) { 22 | throw new Error('Invalid date input'); 23 | } 24 | 25 | // Extract UTC components 26 | const day = dateObj.getUTCDate().toString().padStart(2, '0'); 27 | const month = (dateObj.getUTCMonth() + 1).toString().padStart(2, '0'); 28 | const year = dateObj.getUTCFullYear().toString().slice(-2); 29 | 30 | // Get hours in 12-hour format 31 | let hours = dateObj.getUTCHours(); 32 | const ampm = hours >= 12 ? 'PM' : 'AM'; 33 | hours = hours % 12 || 12; // Convert 0 to 12 34 | 35 | const minutes = dateObj.getUTCMinutes().toString().padStart(2, '0'); 36 | 37 | return `${day}/${month}/${year} - ${hours}:${minutes} ${ampm} UTC`; 38 | } 39 | 40 | /** 41 | * Returns the current UTC time in formatted string: [dd/mm/yy - HH:MM AM/PM UTC] 42 | * @returns Formatted current UTC timestamp string with brackets 43 | */ 44 | export function getCurrentTimestamp(): string { 45 | const now = new Date(); 46 | return `[${formatTimestamp(now)}]`; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/tests/chatRoomTest.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '../agents/Agent'; 2 | import { createLoggerServer } from '../gui/loggerServer'; 3 | import { Logger } from '../utils/logger'; 4 | 5 | // Enable logging before starting the test 6 | Logger.enable(); 7 | 8 | // Wrap everything in an async IIFE 9 | (async () => { 10 | // Initialize and start logger server inside the async context 11 | const loggerServer = createLoggerServer(); 12 | await loggerServer.start(); 13 | 14 | // Create two instances of ChatAgent with different personalities 15 | const agent1 = new Agent({ agentName: "ChatAgent" }); 16 | const agent2 = new Agent({ agentName: "ChatAgent" }); 17 | 18 | // Initialize the conversation with a message 19 | let currentMessage = "THE TREASURE IS MINE!!!"; 20 | 21 | // Track which agent is currently responding 22 | let isAgent1Turn = true; 23 | 24 | // Run the conversation for 10 turns (5 exchanges between agents) 25 | for (let i = 0; i < 10; i++) { 26 | // Select the current agent based on turn 27 | const currentAgent = isAgent1Turn ? agent1 : agent2; 28 | 29 | // Log which agent is responding 30 | console.log(`\n${isAgent1Turn ? 'Agent 1' : 'Agent 2'} is responding to: "${currentMessage}"`); 31 | 32 | // Get the agent's response 33 | const agentResult = await currentAgent.run(currentMessage); 34 | 35 | if (agentResult.success) { 36 | currentMessage = agentResult.output; 37 | console.log(`${isAgent1Turn ? 'Agent 1' : 'Agent 2'} responded:`, currentMessage); 38 | } else { 39 | console.error(`${isAgent1Turn ? 'Agent 1' : 'Agent 2'} failed:`, agentResult.error); 40 | break; 41 | } 42 | 43 | // Switch turns between agents 44 | isAgent1Turn = !isAgent1Turn; 45 | } 46 | })(); -------------------------------------------------------------------------------- /src/tests/terminalLoop.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import { Logger } from '../utils/logger'; 5 | import { createLoggerServer } from '../gui/loggerServer'; 6 | import { TerminalCore } from '../terminal/terminalCore'; 7 | import InternetFeature from '../features/internet'; 8 | import TwitterFeature from '../features/twitter'; 9 | 10 | // Main async function to handle all async operations 11 | async function main() { 12 | Logger.enable(); 13 | Logger.setLevel('debug'); 14 | 15 | const loggerServer = createLoggerServer(); 16 | await loggerServer.start(); 17 | 18 | // Initialize TerminalCore with desired options and features 19 | const core = new TerminalCore({ 20 | maxActions: 30, 21 | actionCooldownMs: 2000, 22 | features: [InternetFeature, TwitterFeature], 23 | }); 24 | 25 | // Add event listeners for loop events 26 | core.on('loop:iteration', async (messages) => { 27 | Logger.info('New messages found to save to database', { 28 | assistantMessage: messages.assistantMessage?.content, 29 | userMessage: messages.userMessage?.content, 30 | }); 31 | }); 32 | 33 | core.on('loop:maxActions', async (fullHistory) => { 34 | Logger.info('Max actions reached !!!', fullHistory); 35 | }); 36 | 37 | await core.init(); 38 | 39 | const externalCurrentSummaries = "Freshly launched"; 40 | 41 | // Set dynamic variables before starting the loop 42 | core.setDynamicVariables({ 43 | additional_dynamic_variables: `## CURRENT SUMMARIES OF YOUR RECENT ACTIVITY\n\n${externalCurrentSummaries}` 44 | }); 45 | 46 | await core.runLoop(); 47 | } 48 | 49 | // Execute the main function and handle any errors 50 | main().catch(error => { 51 | console.error('Error in main:', error); 52 | process.exit(1); 53 | }); -------------------------------------------------------------------------------- /src/types/agentSystem.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export interface Message { 4 | role: 'system' | 'assistant' | 'user' | 'function'; 5 | content?: string; 6 | name?: string; 7 | image?: { 8 | name: string; 9 | mime: string; 10 | data: Buffer | string; 11 | }; 12 | runData?: any; 13 | } 14 | 15 | export interface AgentConfig { 16 | name: string; 17 | description: string; 18 | systemPromptTemplate: string; 19 | dynamicVariables?: { [key: string]: string }; 20 | } 21 | 22 | export interface Tool { 23 | type: 'function'; 24 | function: { 25 | name: string; 26 | strict?: boolean; 27 | description: string; 28 | parameters: Record; 29 | }; 30 | } 31 | 32 | export type ModelType = 'openai' | 'fireworks' | 'anthropic'; 33 | 34 | export interface ModelClient { 35 | modelType: ModelType; 36 | modelName: string; 37 | chatCompletion(params: any): Promise; 38 | } 39 | 40 | export interface FunctionCall { 41 | functionName: string; 42 | functionArgs: Record; 43 | } 44 | 45 | export interface ProcessedResponse { 46 | aiMessage?: { 47 | role: string; 48 | content: string; 49 | }; 50 | functionCalls: FunctionCall[]; 51 | } 52 | 53 | // Base interface for all tool outputs 54 | export type BaseToolOutput = Record; 55 | 56 | // Type to extract Zod schema type 57 | export type ZodSchemaType = z.infer; 58 | 59 | // Type to get tool output type from schema 60 | export type ToolOutputFromSchema = z.infer; 61 | 62 | // Generic type for agent run results 63 | export interface AgentRunResult { 64 | success: boolean; 65 | output: T extends z.ZodTypeAny ? ToolOutputFromSchema : string; 66 | error?: string; 67 | } -------------------------------------------------------------------------------- /src/features/twitter/commands/retweet.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { retweet } from '../../../twitter/functions/retweet'; 3 | import { isCooldownActive } from '../../../supabase/functions/twitter/cooldowns'; 4 | 5 | /** 6 | * @command twitter-retweet 7 | * @description Retweets a specified tweet 8 | */ 9 | export const twitterRetweet: Command = { 10 | name: 're-tweet', 11 | description: 'Retweet a tweet. Only input the tweet ID number, raw digits. An agent will handle the rest.', 12 | parameters: [ 13 | { 14 | name: 'tweetId', 15 | description: 'ID of the tweet to retweet', 16 | required: true, 17 | type: 'string' 18 | } 19 | ], 20 | handler: async (args) => { 21 | // Check for retweet cooldown 22 | const cooldownInfo = await isCooldownActive('retweet'); 23 | 24 | if (cooldownInfo.isActive) { 25 | return { 26 | output: `❌ Action: Retweet\n` + 27 | `Tweet ID: ${args.tweetId}\n` + 28 | 'Status: Failed\n' + 29 | `Reason: Retweet cooldown is active. Please wait ${cooldownInfo.remainingTime} minutes before retweeting again.` 30 | }; 31 | } 32 | 33 | try { 34 | // Proceed with retweeting 35 | const result = await retweet(args.tweetId); 36 | 37 | return { 38 | output: `${result.success ? '✅' : '❌'} Action: Retweet\n` + 39 | `Tweet ID: ${args.tweetId}\n` + 40 | `Status: ${result.success ? 'Success' : 'Failed'}\n` + 41 | `Details: ${result.message}` 42 | }; 43 | } catch (error) { 44 | return { 45 | output: `❌ Action: Retweet\n` + 46 | `Tweet ID: ${args.tweetId}\n` + 47 | `Status: Error\n` + 48 | `Details: ${error}` 49 | }; 50 | } 51 | } 52 | }; -------------------------------------------------------------------------------- /src/features/twitter/commands/quote.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { quoteTweet } from '../../../twitter/functions/quoteTweet'; 3 | 4 | /** 5 | * @command twitter-quote 6 | * @description Creates a quote tweet 7 | */ 8 | export const twitterQuote: Command = { 9 | name: 'quote-tweet', 10 | description: 'Creates a quote tweet with optional media attachments', 11 | parameters: [ 12 | { 13 | name: 'tweetId', 14 | description: 'ID of the tweet to quote', 15 | required: true, 16 | type: 'string' 17 | }, 18 | { 19 | name: 'text', 20 | description: 'Text content of your quote tweet', 21 | required: true, 22 | type: 'string' 23 | }, 24 | { 25 | name: 'mediaUrls', 26 | description: 'Comma-separated list of media URLs (images, GIFs, or videos)', 27 | required: false, 28 | type: 'string' 29 | } 30 | ], 31 | handler: async (args) => { 32 | try { 33 | const mediaUrls = args.mediaUrls ? args.mediaUrls.split(',').map((url: string) => url.trim()) : undefined; 34 | const result = await quoteTweet(args.tweetId, args.text, mediaUrls); 35 | 36 | return { 37 | output: `${result.success ? '✅' : '❌'} Action: Quote Tweet\n` + 38 | `Quoted Tweet ID: ${args.tweetId}\n` + 39 | `${result.tweetId ? `New Tweet ID: ${result.tweetId}\n` : ''}` + 40 | `Status: ${result.success ? 'Success' : 'Failed'}\n` + 41 | `Text: ${args.text}\n` + 42 | `Media: ${mediaUrls ? mediaUrls.join(', ') : 'None'}\n` + 43 | `Details: ${result.message}` 44 | }; 45 | } catch (error) { 46 | return { 47 | output: `❌ Action: Quote Tweet\n` + 48 | `Quoted Tweet ID: ${args.tweetId}\n` + 49 | `Status: Error\n` + 50 | `Details: ${error}` 51 | }; 52 | } 53 | } 54 | }; -------------------------------------------------------------------------------- /src/features/twitter/commands/reply.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../terminal/types/commands'; 2 | import { replyToTweet } from '../../../twitter/functions/replyToTweet'; 3 | 4 | /** 5 | * @command twitter-reply 6 | * @description Replies to a specified tweet 7 | */ 8 | export const twitterReply: Command = { 9 | name: 'reply-to-tweet', 10 | description: 'Replies to a specified tweet with optional media attachments', 11 | parameters: [ 12 | { 13 | name: 'tweetId', 14 | description: 'ID of the tweet to reply to', 15 | required: true, 16 | type: 'string' 17 | }, 18 | { 19 | name: 'text', 20 | description: 'Text content of your reply', 21 | required: true, 22 | type: 'string' 23 | }, 24 | { 25 | name: 'mediaUrls', 26 | description: 'Comma-separated list of media URLs (images, GIFs, or videos)', 27 | required: false, 28 | type: 'string' 29 | } 30 | ], 31 | handler: async (args) => { 32 | try { 33 | const mediaUrls = args.mediaUrls ? args.mediaUrls.split(',').map((url: string) => url.trim()) : undefined; 34 | const result = await replyToTweet(args.tweetId, args.text, mediaUrls); 35 | 36 | return { 37 | output: `${result.success ? '✅' : '❌'} Action: Reply Tweet\n` + 38 | `Parent Tweet ID: ${args.tweetId}\n` + 39 | `${result.tweetId ? `Reply Tweet ID: ${result.tweetId}\n` : ''}` + 40 | `Status: ${result.success ? 'Success' : 'Failed'}\n` + 41 | `Text: ${args.text}\n` + 42 | `Media: ${mediaUrls ? mediaUrls.join(', ') : 'None'}\n` + 43 | `Details: ${result.message}` 44 | }; 45 | } catch (error) { 46 | return { 47 | output: `❌ Action: Reply Tweet\n` + 48 | `Parent Tweet ID: ${args.tweetId}\n` + 49 | `Status: Error\n` + 50 | `Details: ${error}` 51 | }; 52 | } 53 | } 54 | }; -------------------------------------------------------------------------------- /src/tests/manualBackrooms.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | import { createLoggerServer, registerAgentInstance } from '../gui/loggerServer'; 4 | import { Logger } from '../utils/logger'; 5 | import { Agent } from '../agents/Agent'; 6 | import readline from 'readline'; 7 | 8 | async function main() { 9 | // Initialize logging 10 | Logger.enable(); 11 | Logger.setLevel('debug'); 12 | 13 | // Start the logger GUI server 14 | const loggerServer = createLoggerServer(); 15 | await loggerServer.start(); 16 | 17 | // Initialize the CLI agent only 18 | const cliAgent = new Agent({ agentName: 'cliAgent' }); 19 | 20 | // Once the agent is registered by BaseAgent internally, we can get its ID from agentEventBus 21 | // Wait briefly for the agentEventBus to register 22 | const timeout = setTimeout(() => { 23 | // Find the agent ID that matches this newly created cliAgent 24 | // The agentEventBus registers the agent and returns an ID at BaseAgent construction 25 | // So we can find it by name: 26 | const allAgents = require('../utils/agentEventBus').agentEventBus.getAllAgents(); 27 | const match = allAgents.find((a: { name: string; id: string }) => a.name.includes('cliAgent')); 28 | if (match) { 29 | registerAgentInstance(match.id, cliAgent); 30 | } else { 31 | console.warn('cliAgent not found in agentEventBus.'); 32 | } 33 | }, 500); 34 | 35 | timeout.unref(); // Allow process to exit before timeout completes 36 | 37 | // Instead of using readline for terminal input, we skip that: 38 | // The GUI now can send messages to /agent/:id/message and receive responses 39 | console.log("\nThe manualBackrooms agent is now running. Open the GUI at http://localhost:3000 to chat.\n"); 40 | 41 | // Handle cleanup on exit 42 | process.on('SIGINT', async () => { 43 | console.log('\nShutting down...'); 44 | await loggerServer.stop(); 45 | process.exit(0); 46 | }); 47 | } 48 | 49 | main().catch(error => { 50 | console.error('Error in main:', error); 51 | process.exit(1); 52 | }); -------------------------------------------------------------------------------- /src/supabase/functions/memory/learnings.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | import { configLoader } from '../../../utils/config'; 4 | 5 | const agentName = configLoader.getAgentName().toLowerCase(); 6 | const selfFieldName = `${agentName}_self` as const; 7 | 8 | type LearningType = 'world_knowledge' | 'crypto_ecosystem_knowledge' | 'user_specific' | typeof selfFieldName; 9 | 10 | interface LearningEntry { 11 | id: number; 12 | session_id: string | null; 13 | user_id: string | null; 14 | learning_type: LearningType; 15 | content: string; 16 | created_at?: string; 17 | } 18 | 19 | export class Learnings { 20 | static async saveLearning( 21 | learningType: LearningType, 22 | content: string, 23 | sessionId: string | null, 24 | userId: string | null = null 25 | ): Promise { 26 | try { 27 | await supabase 28 | .from('learnings') 29 | .insert({ 30 | learning_type: learningType, 31 | content, 32 | session_id: sessionId, 33 | user_id: userId, 34 | }); 35 | Logger.info(`Successfully saved learning of type: ${learningType}`); 36 | } catch (error) { 37 | Logger.error('Error saving learning:', error); 38 | } 39 | } 40 | 41 | static async getLearningsByType( 42 | learningType: LearningType, 43 | sessionId: string | null = null 44 | ): Promise { 45 | try { 46 | let query = supabase 47 | .from('learnings') 48 | .select('*') 49 | .eq('learning_type', learningType); 50 | 51 | if (sessionId) { 52 | query = query.eq('session_id', sessionId); 53 | } 54 | 55 | const { data, error } = await query; 56 | 57 | if (error) { 58 | Logger.error('Error retrieving learnings:', error); 59 | return []; 60 | } 61 | 62 | return data as LearningEntry[]; 63 | } catch (error) { 64 | Logger.error('Error retrieving learnings:', error); 65 | return []; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/supabase/functions/twitter/followEntries.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | import { findTwitterUserByTwitterId, createTwitterUser } from './userEntries'; 4 | import { getTwitterUserInfo } from '../../../twitter/utils/profileUtils'; 5 | 6 | export async function isUserFollowedByBot(username: string): Promise { 7 | try { 8 | const { data } = await supabase 9 | .from('user_accounts') 10 | .select(` 11 | id, 12 | twitter_user_accounts!inner ( 13 | is_followed_by_bot 14 | ) 15 | `) 16 | .eq('platform', 'twitter') 17 | .eq('username', username) 18 | .maybeSingle(); 19 | 20 | return data?.twitter_user_accounts?.is_followed_by_bot || false; 21 | } catch (error) { 22 | Logger.error('Error checking if user is followed:', error); 23 | return false; 24 | } 25 | } 26 | 27 | export async function updateUserFollowStatus( 28 | username: string, 29 | twitterId: string 30 | ): Promise { 31 | try { 32 | let userResult = await findTwitterUserByTwitterId(twitterId); 33 | 34 | if (!userResult) { 35 | Logger.info(`New user detected: @${username}. Fetching profile info...`); 36 | const userInfo = await getTwitterUserInfo(username); 37 | 38 | if (!userInfo) { 39 | Logger.error('Failed to get Twitter profile info'); 40 | return false; 41 | } 42 | 43 | userResult = await createTwitterUser(username, twitterId, userInfo.profile); 44 | if (!userResult) { 45 | Logger.error('Failed to create user record'); 46 | return false; 47 | } 48 | } 49 | 50 | const { error } = await supabase 51 | .from('twitter_user_accounts') 52 | .update({ 53 | is_followed_by_bot: true, 54 | last_followed_at: new Date().toISOString() 55 | }) 56 | .eq('user_account_id', userResult.userAccountId); 57 | 58 | if (error) { 59 | Logger.error('Error updating follow status:', error); 60 | return false; 61 | } 62 | 63 | return true; 64 | } catch (error) { 65 | Logger.error('Error in updateUserFollowStatus:', error); 66 | return false; 67 | } 68 | } -------------------------------------------------------------------------------- /src/twitter/functions/getTweets.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import type { Tweet } from 'goat-x'; 3 | import { formatTimestamp } from '../../utils/formatTimestamps'; 4 | import { hasInteractedWithTweet, debugTweetInteractions } from '../../supabase/functions/twitter/tweetInteractionChecks'; 5 | import { Logger } from '../../utils/logger'; 6 | 7 | /** 8 | * Gets recent tweets from a specific user 9 | * @param username - Twitter username (without @ symbol) 10 | * @param maxTweets - Maximum number of tweets to fetch 11 | * @returns Array of formatted tweet strings 12 | */ 13 | export async function getTweets(username: string, maxTweets: number): Promise { 14 | try { 15 | Logger.info(`Fetching tweets from @${username}...`); 16 | const rawTweets: Tweet[] = []; 17 | 18 | // First collect all raw tweets 19 | for await (const tweet of scraper.getTweets(username, maxTweets)) { 20 | rawTweets.push(tweet); 21 | } 22 | 23 | Logger.info(`Found ${rawTweets.length} total tweets, checking for previous interactions...`); 24 | 25 | // Filter out already interacted tweets 26 | const unhandledTweets = await Promise.all( 27 | rawTweets.map(async (tweet) => { 28 | const hasInteracted = await hasInteractedWithTweet(tweet.id!); 29 | if (hasInteracted) { 30 | await debugTweetInteractions(tweet.id!); 31 | Logger.info(`Filtering out tweet ${tweet.id} - already interacted with`); 32 | return null; 33 | } 34 | return tweet; 35 | }) 36 | ); 37 | 38 | // Format remaining tweets 39 | const formattedTweets = unhandledTweets 40 | .filter((tweet): tweet is Tweet => tweet !== null) 41 | .map(tweet => { 42 | const timestamp = tweet.timeParsed ? 43 | formatTimestamp(new Date(tweet.timeParsed)) : 44 | 'Unknown time'; 45 | 46 | return `- [${tweet.id}] @${tweet.username || 'unknown_user'} (${timestamp}): ${tweet.text}`; 47 | }); 48 | 49 | Logger.info(`Returning ${formattedTweets.length} formatted tweets after filtering`); 50 | return formattedTweets; 51 | 52 | } catch (error) { 53 | Logger.error('Error fetching tweets:', error); 54 | return []; 55 | } 56 | } -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | interface LogEntry { 4 | level: string; 5 | timestamp: string; 6 | messages: (string | object)[]; 7 | } 8 | 9 | // Define log levels and their priorities 10 | const LOG_LEVELS = { 11 | none: 0, 12 | error: 1, 13 | info: 2, 14 | debug: 3 15 | } as const; 16 | 17 | type LogLevel = keyof typeof LOG_LEVELS; 18 | 19 | class LoggerClass extends EventEmitter { 20 | private logs: LogEntry[] = []; 21 | private enabled: boolean = false; 22 | private currentLevel: LogLevel = 'none'; 23 | 24 | constructor() { 25 | super(); 26 | } 27 | 28 | enable() { 29 | this.enabled = true; 30 | } 31 | 32 | disable() { 33 | this.enabled = false; 34 | } 35 | 36 | setLevel(level: LogLevel) { 37 | this.currentLevel = level; 38 | } 39 | 40 | private shouldLog(level: LogLevel): boolean { 41 | // Don't log if logger is disabled or if current level is none 42 | if (!this.enabled || this.currentLevel === 'none') { 43 | return false; 44 | } 45 | 46 | // Log if the message's level priority is <= current level priority 47 | return LOG_LEVELS[level] <= LOG_LEVELS[this.currentLevel]; 48 | } 49 | 50 | private storeLog(level: 'error' | 'info' | 'debug', ...args: any[]) { 51 | if (!this.shouldLog(level)) return; 52 | 53 | const entry: LogEntry = { 54 | level, 55 | timestamp: new Date().toISOString(), 56 | messages: args 57 | }; 58 | this.logs.push(entry); 59 | this.emit('log', entry); 60 | } 61 | 62 | getLogs(): LogEntry[] { 63 | return this.logs; 64 | } 65 | 66 | clearLogs() { 67 | this.logs = []; 68 | } 69 | 70 | error(...args: any[]) { 71 | if (this.shouldLog('error')) { 72 | console.error(...args); 73 | this.storeLog('error', ...args); 74 | } 75 | } 76 | 77 | info(...args: any[]) { 78 | if (this.shouldLog('info')) { 79 | console.info(...args); 80 | this.storeLog('info', ...args); 81 | } 82 | } 83 | 84 | debug(...args: any[]) { 85 | if (this.shouldLog('debug')) { 86 | console.debug(...args); 87 | this.storeLog('debug', ...args); 88 | } 89 | } 90 | } 91 | 92 | // Export singleton instance 93 | export const Logger = new LoggerClass(); -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from 'js-yaml'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { Logger } from './logger'; 5 | 6 | // Interface for agent configuration 7 | export interface AgentConfig { 8 | name: string; 9 | raw_personality: string; 10 | main_goal: string; 11 | } 12 | 13 | // Interface for the entire configuration 14 | interface Config { 15 | agent: AgentConfig; 16 | ticker?: string; 17 | tickerName?: string; 18 | bannedPhrases?: string[]; 19 | imageGen?: { 20 | loraPath: string; 21 | promptPrefix: string; 22 | triggerToken: string; 23 | }; 24 | } 25 | 26 | class ConfigLoader { 27 | private static instance: ConfigLoader; 28 | private config!: Config; 29 | private configPath: string; 30 | 31 | private constructor() { 32 | // Default config path 33 | this.configPath = path.join(__dirname, '..', 'config', 'agent.yaml'); 34 | this.loadConfig(); 35 | } 36 | 37 | private loadConfig() { 38 | try { 39 | const fileContents = fs.readFileSync(this.configPath, 'utf8'); 40 | this.config = yaml.load(fileContents) as Config; 41 | Logger.info('Configuration loaded successfully'); 42 | } catch (error) { 43 | Logger.error(`Error loading configuration: ${error}`); 44 | throw error; 45 | } 46 | } 47 | 48 | // Add method to initialize with custom path 49 | public initializeWithPath(customPath: string) { 50 | this.configPath = customPath; 51 | this.loadConfig(); 52 | } 53 | 54 | public static getInstance(): ConfigLoader { 55 | if (!ConfigLoader.instance) { 56 | ConfigLoader.instance = new ConfigLoader(); 57 | } 58 | return ConfigLoader.instance; 59 | } 60 | 61 | public getAgentConfig(): AgentConfig { 62 | return this.config.agent; 63 | } 64 | 65 | public getAgentName(): string { 66 | return this.config.agent.name; 67 | } 68 | 69 | public getRawPersonality(): string { 70 | return this.config.agent.raw_personality.trim(); 71 | } 72 | 73 | public getConfig(): Config { 74 | return this.config; 75 | } 76 | 77 | public getBannedPhrasesFormatted(): string { 78 | return (this.config.bannedPhrases || []) 79 | .map(phrase => `- ${phrase}`) 80 | .join('\n'); 81 | } 82 | } 83 | 84 | export const configLoader = ConfigLoader.getInstance(); -------------------------------------------------------------------------------- /src/tests/toolCalling.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '../agents/Agent'; 2 | import { Logger } from '../utils/logger'; 3 | import { createLoggerServer } from '../gui/loggerServer'; 4 | import { FunctionCall } from '../types/agentSystem'; 5 | 6 | // This is an example of how to simulate tool calling in an agent. API calls are simulated by adding messages to the agent. 7 | 8 | Logger.enable(); 9 | Logger.setLevel('debug'); 10 | 11 | const loggerServer = createLoggerServer(); 12 | loggerServer.start(); 13 | 14 | (async () => { 15 | const agent = new Agent({ agentName: "toolAgent" }); 16 | 17 | const userMessage = "Hey, can you tell me the weather in San Francisco and also the current time in Tokyo?"; 18 | const result = await agent.run(userMessage); 19 | 20 | if (result.success) { 21 | console.log("Initial Agent response:", result.output); 22 | 23 | const resultWithFunctionCalls = result as { success: boolean; output: any; error?: string; functionCalls?: FunctionCall[] }; 24 | 25 | // Check if we have function calls that need to be executed. 26 | if (resultWithFunctionCalls.functionCalls && resultWithFunctionCalls.functionCalls.length > 0) { 27 | // Simulate executing the tools externally: 28 | for (const call of resultWithFunctionCalls.functionCalls) { 29 | if (call.functionName === "get_weather") { 30 | // Simulate calling weather API 31 | const apiResult = { weather: "Sunny, 20°C" }; 32 | // Feed the result back to the agent as a tool response 33 | agent.addUserMessage(JSON.stringify(apiResult)); 34 | } else if (call.functionName === "get_time") { 35 | // Simulate calling time API 36 | const apiResult = { time: "2024-12-15T09:23:00Z" }; 37 | agent.addUserMessage(JSON.stringify(apiResult)); 38 | } 39 | } 40 | 41 | // Now re-run the agent so it can use these tool results to produce a final answer: 42 | const finalResult = await agent.run(); 43 | if (finalResult.success) { 44 | console.log("Final Agent response with tool results:", finalResult.output); 45 | } else { 46 | console.error("Agent error on final run:", finalResult.error); 47 | } 48 | } else { 49 | console.log("No function calls to execute."); 50 | } 51 | } else { 52 | console.error("Agent error:", result.error); 53 | } 54 | })(); -------------------------------------------------------------------------------- /src/tests/cli.ts: -------------------------------------------------------------------------------- 1 | // Simple CLI interface to accept user input 2 | 3 | import readline from 'readline'; 4 | import { executeCommand } from '../terminal/executeCommand'; 5 | import { Logger } from '../utils/logger'; 6 | import { registerCommands } from '../terminal/commandRegistry'; 7 | import { help } from '../terminal/commands/help'; 8 | import { Feature } from '../terminal/types/feature'; 9 | import InternetFeature from '../features/internet'; 10 | import TwitterFeature from '../features/twitter'; 11 | 12 | Logger.enable(); 13 | Logger.setLevel('debug'); 14 | 15 | /** 16 | * Configuration for CLI test environment 17 | * Add or remove features as needed for testing 18 | */ 19 | const cliFeatures: Feature[] = [ 20 | InternetFeature, 21 | TwitterFeature, 22 | // Add other features here 23 | ]; 24 | 25 | interface CLIOptions { 26 | features?: Feature[]; 27 | } 28 | 29 | /** 30 | * Initializes the CLI application for manual use to test terminal functions 31 | * - Sets up readline interface 32 | * - Registers core commands and feature commands 33 | * - Starts accepting commands 34 | * - Logs terminal history 35 | */ 36 | async function initializeCLI(options: CLIOptions = {}) { 37 | try { 38 | const rl = readline.createInterface({ 39 | input: process.stdin, 40 | output: process.stdout, 41 | }); 42 | 43 | // Register core commands 44 | registerCommands([help]); 45 | 46 | // Load and register feature commands 47 | if (options.features) { 48 | for (const feature of options.features) { 49 | const commands = await feature.loadFeatureCommands(); 50 | registerCommands(commands); 51 | } 52 | } 53 | 54 | console.log('\nWelcome to the Terminal. Use "help" to view available commands. Type commands below:'); 55 | 56 | rl.on('line', async (input) => { 57 | const trimmedInput = input.trim(); 58 | if (trimmedInput) { 59 | try { 60 | // Execute the command and get raw result 61 | const result = await executeCommand(trimmedInput); 62 | 63 | // Log the raw output 64 | console.log(result.output); 65 | } catch (error) { 66 | console.error('Command execution failed:', error); 67 | } 68 | } 69 | }); 70 | 71 | // Handle CLI shutdown 72 | rl.on('close', () => { 73 | console.log('\nGoodbye!'); 74 | process.exit(0); 75 | }); 76 | 77 | } catch (error) { 78 | console.error('Failed to initialize CLI:', error); 79 | process.exit(1); 80 | } 81 | } 82 | 83 | // Start the CLI with configured features 84 | initializeCLI({ features: cliFeatures }); -------------------------------------------------------------------------------- /src/models/adapters/FireworksAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Message, Tool } from '../../types/agentSystem'; 2 | import { ModelAdapter, ProcessedResponse, FunctionCall } from './ModelAdapter'; 3 | 4 | export class FireworksAdapter extends ModelAdapter { 5 | public buildParams( 6 | messages: Message[], 7 | tools: Tool[], 8 | toolChoice?: any, 9 | systemPrompt?: string, 10 | outputSchema?: any 11 | ): any { 12 | const formattedMessages = messages.map(m => ({ 13 | role: m.role, 14 | content: m.content || '' 15 | })); 16 | 17 | const params: any = { 18 | model: this.modelName, 19 | messages: formattedMessages 20 | }; 21 | 22 | if (tools && tools.length > 0) { 23 | params.tools = tools.map(t => ({ 24 | type: "function", 25 | function: { 26 | name: t.function.name, 27 | description: t.function.description, 28 | parameters: t.function.parameters 29 | } 30 | })); 31 | } 32 | 33 | if (toolChoice) { 34 | params.tool_choice = toolChoice; 35 | } 36 | 37 | if (outputSchema) { 38 | // Fireworks does not accept strict, so do nothing special 39 | } 40 | 41 | return params; 42 | } 43 | 44 | public formatTools(tools: Tool[]): any[] { 45 | return tools.map(tool => ({ 46 | type: "function", 47 | function: { 48 | name: tool.function.name, 49 | description: tool.function.description, 50 | parameters: tool.function.parameters 51 | } 52 | })); 53 | } 54 | 55 | public buildToolChoice(tools: Tool[]): any { 56 | return "auto"; 57 | } 58 | 59 | public processResponse(response: any): ProcessedResponse { 60 | const choice = response.choices && response.choices[0]; 61 | if (!choice) { 62 | return { functionCalls: [] }; 63 | } 64 | 65 | const message = choice.message || {}; 66 | const aiMessage = message.role === 'assistant' && message.content ? { 67 | role: 'assistant', 68 | content: message.content 69 | } : undefined; 70 | 71 | let functionCalls: FunctionCall[] = []; 72 | 73 | if (message.tool_calls && Array.isArray(message.tool_calls)) { 74 | functionCalls = message.tool_calls.map((tc: any) => ({ 75 | functionName: tc.function.name, 76 | functionArgs: JSON.parse(tc.function.arguments) 77 | })); 78 | } else if (message.function_call) { 79 | functionCalls.push({ 80 | functionName: message.function_call.name, 81 | functionArgs: JSON.parse(message.function_call.arguments) 82 | }); 83 | } 84 | 85 | return { aiMessage, functionCalls }; 86 | } 87 | } -------------------------------------------------------------------------------- /src/supabase/functions/terminal/terminalEntries.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { ToolOutputFromSchema } from '../../../types/agentSystem'; 3 | import { z } from 'zod'; 4 | import { Logger } from '../../../utils/logger'; 5 | 6 | const terminalCommandSchema = z.object({ 7 | command: z.string() 8 | }); 9 | 10 | const terminalToolSchema = z.object({ 11 | internal_thought: z.string(), 12 | plan: z.string(), 13 | terminal_commands: z.array(terminalCommandSchema) 14 | }); 15 | 16 | type TerminalToolOutput = z.infer; 17 | 18 | export async function createTerminalEntry( 19 | sessionId: string, 20 | output: TerminalToolOutput 21 | ) { 22 | try { 23 | const commandsString = output.terminal_commands 24 | .map((cmd: { command: string }) => cmd.command) 25 | .join('\n'); 26 | 27 | const { data: entry } = await supabase 28 | .from('terminal_history') 29 | .insert({ 30 | session_id: sessionId, 31 | internal_thought: output.internal_thought, 32 | plan: output.plan, 33 | command: commandsString, 34 | terminal_log: null 35 | }) 36 | .select('id') 37 | .single(); 38 | 39 | return entry?.id; 40 | } catch (error) { 41 | Logger.error('Error creating terminal entry:', error); 42 | return null; 43 | } 44 | } 45 | 46 | export async function updateTerminalResponse( 47 | entryId: number, 48 | response: string 49 | ) { 50 | try { 51 | const { data } = await supabase 52 | .from('terminal_history') 53 | .update({ terminal_log: response }) 54 | .eq('id', entryId) 55 | .select() 56 | .single(); 57 | 58 | return data?.id; 59 | } catch (error) { 60 | Logger.error('Error updating terminal response:', error); 61 | return null; 62 | } 63 | } 64 | 65 | export async function updateTerminalStatus(isActive: boolean) { 66 | try { 67 | const { data } = await supabase 68 | .from('terminal_status') 69 | .update({ 70 | is_active: isActive, 71 | last_updated: new Date().toISOString() 72 | }) 73 | .eq('id', true) 74 | .select() 75 | .single(); 76 | 77 | return data?.is_active; 78 | } catch (error) { 79 | Logger.error('Error updating terminal status:', error); 80 | return null; 81 | } 82 | } 83 | 84 | export async function getTerminalStatus() { 85 | try { 86 | const { data } = await supabase 87 | .from('terminal_status') 88 | .select('is_active, last_updated') 89 | .eq('id', true) 90 | .single(); 91 | 92 | return data; 93 | } catch (error) { 94 | Logger.error('Error getting terminal status:', error); 95 | return null; 96 | } 97 | } -------------------------------------------------------------------------------- /src/models/adapters/OpenAIAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Message, Tool } from '../../types/agentSystem'; 2 | import { ModelAdapter, ProcessedResponse, FunctionCall } from './ModelAdapter'; 3 | 4 | export class OpenAIAdapter extends ModelAdapter { 5 | public buildParams( 6 | messages: Message[], 7 | tools: Tool[], 8 | toolChoice?: any, 9 | systemPrompt?: string, 10 | outputSchema?: any 11 | ): any { 12 | const formattedMessages = messages.map(m => { 13 | return { role: m.role, content: m.content || '' }; 14 | }); 15 | 16 | const params: any = { 17 | model: this.modelName, 18 | messages: formattedMessages, 19 | max_tokens: 1024, 20 | temperature: 0 21 | }; 22 | 23 | if (tools && tools.length > 0) { 24 | params.tools = tools.map(t => ({ 25 | type: "function", 26 | function: { 27 | name: t.function.name, 28 | description: t.function.description, 29 | parameters: t.function.parameters 30 | } 31 | })); 32 | } 33 | 34 | if (toolChoice) { 35 | params.tool_choice = toolChoice; 36 | } 37 | 38 | if (outputSchema && params.tools) { 39 | // Strict can be turned on for structured outputs 40 | params.tools.forEach((tool: any) => { 41 | tool.function.strict = true; 42 | }); 43 | } 44 | 45 | return params; 46 | } 47 | 48 | public formatTools(tools: Tool[]): any[] { 49 | return tools.map(tool => ({ 50 | type: "function", 51 | function: { 52 | name: tool.function.name, 53 | description: tool.function.description, 54 | parameters: tool.function.parameters 55 | } 56 | })); 57 | } 58 | 59 | public buildToolChoice(tools: Tool[]): any { 60 | return "auto"; 61 | } 62 | 63 | public processResponse(response: any): ProcessedResponse { 64 | const choice = response.choices && response.choices[0]; 65 | if (!choice) { 66 | return { functionCalls: [] }; 67 | } 68 | 69 | const message = choice.message || {}; 70 | const aiMessage = message.role === 'assistant' && message.content ? { 71 | role: 'assistant', 72 | content: message.content 73 | } : undefined; 74 | 75 | let functionCalls: FunctionCall[] = []; 76 | 77 | if (message.tool_calls && Array.isArray(message.tool_calls)) { 78 | functionCalls = message.tool_calls.map((tc: any) => ({ 79 | functionName: tc.function.name, 80 | functionArgs: JSON.parse(tc.function.arguments) 81 | })); 82 | } else if (message.function_call) { 83 | functionCalls.push({ 84 | functionName: message.function_call.name, 85 | functionArgs: JSON.parse(message.function_call.arguments) 86 | }); 87 | } 88 | 89 | return { aiMessage, functionCalls }; 90 | } 91 | } -------------------------------------------------------------------------------- /src/twitter/functions/getHomepage.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { formatTimestamp } from '../../utils/formatTimestamps'; 3 | import { hasInteractedWithTweet, debugTweetInteractions } from '../../supabase/functions/twitter/tweetInteractionChecks'; 4 | import { isUserFollowedByBot } from '../../supabase/functions/twitter/followEntries'; 5 | import { Logger } from '../../utils/logger'; 6 | 7 | /** 8 | * Gets tweets from the homepage timeline 9 | * @param maxTweets - Maximum number of tweets to fetch (default: 20) 10 | * @returns Array of formatted tweet strings 11 | */ 12 | export async function getHomepage(maxTweets: number = 20): Promise { 13 | try { 14 | Logger.info(`Fetching homepage tweets (max: ${maxTweets})...`); 15 | const rawTweets: any[] = []; 16 | const listId = '1621164352186327041'; 17 | 18 | // First collect raw tweets with proper limit 19 | const response = await scraper.fetchListTweets(listId, maxTweets); 20 | if (!response || !response.tweets || response.tweets.length === 0) { 21 | Logger.info('No tweets found in response'); 22 | return []; 23 | } 24 | 25 | // Only take up to maxTweets tweets 26 | rawTweets.push(...response.tweets.slice(0, maxTweets)); 27 | Logger.info(`Found ${rawTweets.length}/${maxTweets} tweets, checking for previous interactions...`); 28 | 29 | // Filter out already interacted tweets and check following status 30 | const unhandledTweets = await Promise.all( 31 | rawTweets.map(async (tweet) => { 32 | const hasInteracted = await hasInteractedWithTweet(tweet.id!); 33 | if (hasInteracted) { 34 | await debugTweetInteractions(tweet.id!); 35 | Logger.info(`Filtering out tweet ${tweet.id} - already interacted with`); 36 | return null; 37 | } 38 | 39 | // Check if we're following the user 40 | const isFollowing = await isUserFollowedByBot(tweet.username || ''); 41 | return { ...tweet, isFollowing }; 42 | }) 43 | ); 44 | 45 | // Format remaining tweets 46 | const formattedTweets = unhandledTweets 47 | .filter((tweet): tweet is any => tweet !== null) 48 | .map(tweet => { 49 | const timestamp = tweet.timeParsed ? 50 | formatTimestamp(new Date(tweet.timeParsed)) : 51 | 'Unknown time'; 52 | 53 | const followStatus = tweet.isFollowing ? '(FOLLOWING)' : '(NOT FOLLOWING)'; 54 | 55 | return `- [${tweet.id}] @${tweet.username || 'unknown_user'} ${followStatus} (${timestamp}): ${tweet.text}`; 56 | }); 57 | 58 | Logger.info(`Returning ${formattedTweets.length} formatted tweets after filtering`); 59 | return formattedTweets; 60 | 61 | } catch (error) { 62 | Logger.error('Error fetching homepage tweets:', error); 63 | return []; 64 | } 65 | } -------------------------------------------------------------------------------- /src/twitter/functions/sendTweet.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { prepareMediaData } from '../utils/mediaUtils'; 3 | import { logTweet } from '../../supabase/functions/twitter/tweetEntries'; 4 | import { Logger } from '../../utils/logger'; 5 | 6 | /** 7 | * Extracts tweet ID from response based on tweet type 8 | * @param responseData - API response data 9 | * @param isLongTweet - Whether this was a long tweet 10 | * @returns Tweet ID or null 11 | */ 12 | function extractTweetId(responseData: any, isLongTweet: boolean): string | null { 13 | try { 14 | if (isLongTweet) { 15 | // Path for long tweets (notetweets) 16 | return responseData?.data?.notetweet_create?.tweet_results?.result?.rest_id; 17 | } else { 18 | // Path for regular tweets 19 | return responseData?.data?.create_tweet?.tweet_results?.result?.rest_id; 20 | } 21 | } catch (error) { 22 | Logger.error('Error extracting tweet ID:', error); 23 | return null; 24 | } 25 | } 26 | 27 | /** 28 | * Sends a main tweet with optional media and logs it to the database. 29 | * @param text - The text content of the tweet 30 | * @param mediaUrls - Optional array of media URLs 31 | * @returns The ID of the sent tweet, or null if failed 32 | */ 33 | export async function sendTweet( 34 | text: string, 35 | mediaUrls?: string[] 36 | ): Promise { 37 | try { 38 | // Prepare media data for Twitter API 39 | const mediaData = mediaUrls ? await prepareMediaData(mediaUrls) : undefined; 40 | 41 | // Check if tweet exceeds standard character limit 42 | const isLongTweet = text.length > 280; 43 | 44 | // Send tweet using appropriate method based on length 45 | const response = isLongTweet 46 | ? await scraper.sendLongTweet(text, undefined, mediaData) 47 | : await scraper.sendTweet(text, undefined, mediaData); 48 | 49 | Logger.info("RAW RESPONSE", response); 50 | const responseData = await response.json(); 51 | const tweetId = extractTweetId(responseData, isLongTweet); 52 | 53 | if (tweetId) { 54 | Logger.info(`${isLongTweet ? 'Long tweet' : 'Tweet'} sent successfully (ID: ${tweetId})`); 55 | 56 | // Log the tweet to the database with prepared media data 57 | const logResult = await logTweet({ 58 | tweet_id: tweetId, 59 | text: text, 60 | tweet_type: 'main', 61 | has_media: !!mediaData, 62 | created_at: new Date().toISOString() 63 | }, mediaData); 64 | 65 | if (logResult) { 66 | Logger.info(`Tweet logged with ID: ${logResult}`); 67 | } else { 68 | Logger.info('Failed to log tweet to Supabase.'); 69 | } 70 | 71 | return tweetId; 72 | } else { 73 | Logger.error('Failed to retrieve tweet ID from response:', responseData); 74 | return null; 75 | } 76 | } catch (error) { 77 | Logger.error('Error sending tweet:', error); 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/twitter/functions/searchTwitter.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { SearchMode } from 'goat-x'; 3 | import type { Tweet } from 'goat-x'; 4 | import { formatTimestamp } from '../../utils/formatTimestamps'; 5 | import { hasInteractedWithTweet, debugTweetInteractions } from '../../supabase/functions/twitter/tweetInteractionChecks'; 6 | import { isUserFollowedByBot } from '../../supabase/functions/twitter/followEntries'; 7 | import { Logger } from '../../utils/logger'; 8 | 9 | /** 10 | * Searches Twitter for tweets matching a query 11 | * @param query - Search query string 12 | * @param maxResults - Maximum number of results to return (default: 20) 13 | * @returns Array of formatted tweet strings 14 | */ 15 | export async function searchTwitter(query: string, maxResults: number = 20): Promise { 16 | try { 17 | Logger.info(`Searching Twitter for: "${query}"...`); 18 | const rawTweets: Tweet[] = []; 19 | const searchMode = SearchMode.Latest; 20 | 21 | // First collect all raw tweets 22 | for await (const tweet of scraper.searchTweets(query, maxResults, searchMode)) { 23 | // Skip tweets from the bot itself 24 | if (tweet.username === process.env.TWITTER_USERNAME) continue; 25 | rawTweets.push(tweet); 26 | } 27 | 28 | Logger.info(`Found ${rawTweets.length} total results, checking for previous interactions...`); 29 | 30 | // Filter out already interacted tweets and check following status 31 | const unhandledTweets = await Promise.all( 32 | rawTweets.map(async (tweet) => { 33 | const hasInteracted = await hasInteractedWithTweet(tweet.id!); 34 | if (hasInteracted) { 35 | await debugTweetInteractions(tweet.id!); 36 | Logger.info(`Filtering out tweet ${tweet.id} - already interacted with`); 37 | return null; 38 | } 39 | 40 | // Check if we're following the user 41 | const isFollowing = await isUserFollowedByBot(tweet.username || ''); 42 | return { ...tweet, isFollowing }; 43 | }) 44 | ); 45 | 46 | const validTweets = unhandledTweets.filter((tweet): tweet is (Tweet & { isFollowing: boolean }) => tweet !== null); 47 | 48 | if (validTweets.length === 0) { 49 | return []; 50 | } 51 | 52 | // Format remaining tweets 53 | const formattedTweets = validTweets 54 | .map(tweet => { 55 | const timestamp = tweet.timeParsed ? 56 | formatTimestamp(new Date(tweet.timeParsed)) : 57 | 'Unknown time'; 58 | 59 | const followStatus = tweet.isFollowing ? '(FOLLOWING)' : '(NOT FOLLOWING)'; 60 | 61 | return `- [${tweet.id}] @${tweet.username || 'unknown_user'} ${followStatus} (${timestamp}): ${tweet.text}`; 62 | }); 63 | 64 | Logger.info(`Returning ${formattedTweets.length} formatted tweets after filtering`); 65 | return formattedTweets; 66 | 67 | } catch (error) { 68 | Logger.error('Error searching tweets:', error); 69 | return []; 70 | } 71 | } -------------------------------------------------------------------------------- /src/twitter/utils/tweetUtils.ts: -------------------------------------------------------------------------------- 1 | import { Profile, Tweet } from 'goat-x'; 2 | import { Logger } from '../../utils/logger'; 3 | import { scraper } from '../twitterClient'; 4 | 5 | /** 6 | * Analyzes a tweet to determine how it relates to the bot 7 | */ 8 | export async function analyzeTweetContext(tweet: Tweet): Promise<{ 9 | type: 'mention' | 'reply_to_bot' | 'reply_to_others' | null; 10 | parentTweetId?: string; 11 | parentTweetAuthor?: string; 12 | }> { 13 | try { 14 | const botUsername = process.env.TWITTER_USERNAME; 15 | Logger.info('Analyzing tweet context:', { 16 | isReply: tweet.isReply, 17 | inReplyToStatusId: tweet.inReplyToStatusId, 18 | mentions: tweet.mentions, 19 | botUsername 20 | }); 21 | 22 | // First check if it's a reply 23 | if (tweet.isReply && tweet.inReplyToStatusId) { 24 | Logger.info('Getting parent tweet:', tweet.inReplyToStatusId); 25 | const parentTweet = await scraper.getTweet(tweet.inReplyToStatusId); 26 | Logger.info('Parent tweet:', { 27 | found: !!parentTweet, 28 | username: parentTweet?.username, 29 | text: parentTweet?.text 30 | }); 31 | 32 | if (parentTweet?.username) { 33 | const isReplyToBot = parentTweet.username.toLowerCase() === botUsername?.toLowerCase(); 34 | const mentionsBot = tweet.mentions?.some(m => 35 | m.username?.toLowerCase() === botUsername?.toLowerCase() 36 | ); 37 | 38 | Logger.info('Context analysis:', { 39 | isReplyToBot, 40 | mentionsBot, 41 | parentUsername: parentTweet.username 42 | }); 43 | 44 | // If replying to bot's tweet 45 | if (isReplyToBot) { 46 | return { 47 | type: 'reply_to_bot', 48 | parentTweetId: tweet.inReplyToStatusId, 49 | parentTweetAuthor: parentTweet.username 50 | }; 51 | } 52 | 53 | // If it's a reply and mentions bot 54 | if (mentionsBot) { 55 | return { 56 | type: 'mention', 57 | parentTweetId: tweet.inReplyToStatusId, 58 | parentTweetAuthor: parentTweet.username 59 | }; 60 | } 61 | 62 | // Regular reply to someone else 63 | return { 64 | type: 'reply_to_others', 65 | parentTweetId: tweet.inReplyToStatusId, 66 | parentTweetAuthor: parentTweet.username 67 | }; 68 | } 69 | } 70 | 71 | // Not a reply but mentions bot 72 | const mentionsBot = tweet.mentions?.some(m => 73 | m.username?.toLowerCase() === botUsername?.toLowerCase() 74 | ); 75 | if (mentionsBot) { 76 | Logger.info('Tweet is a direct mention'); 77 | return { type: 'mention' }; 78 | } 79 | 80 | // Main tweet with no bot mention 81 | Logger.info('Tweet is a main tweet with no bot context'); 82 | return { type: null }; 83 | } catch (error) { 84 | Logger.error('Error analyzing tweet context:', error); 85 | return { type: null }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/twitter/functions/followUser.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { Logger } from '../../utils/logger'; 3 | import { isUserFollowedByBot, updateUserFollowStatus } from '../../supabase/functions/twitter/followEntries'; 4 | 5 | // Define possible follow result statuses 6 | export type FollowResultStatus = 'success' | 'already_following' | 'user_not_found' | 'error'; 7 | 8 | // Define the follow result type 9 | export interface FollowResult { 10 | status: FollowResultStatus; 11 | message: string; 12 | error?: any; 13 | } 14 | 15 | /** 16 | * Follows a specific user if not already followed 17 | * @param username - The username of the account to follow 18 | * @returns Promise with detailed status information 19 | */ 20 | export async function followUser(username: string): Promise { 21 | try { 22 | // Check if user is already followed 23 | const isFollowed = await isUserFollowedByBot(username); 24 | if (isFollowed) { 25 | return { 26 | status: 'already_following', 27 | message: `Bot is already following @${username}` 28 | }; 29 | } 30 | 31 | // Get basic user info first 32 | const userId = await scraper.getUserIdByScreenName(username); 33 | if (!userId) { 34 | return { 35 | status: 'user_not_found', 36 | message: `Could not find user @${username} on Twitter` 37 | }; 38 | } 39 | 40 | // Attempt to follow the user 41 | try { 42 | await scraper.followUser(username); 43 | Logger.info(`Successfully followed user @${username}`); 44 | } catch (error) { 45 | return { 46 | status: 'error', 47 | message: `Failed to follow @${username} on Twitter`, 48 | error 49 | }; 50 | } 51 | 52 | // Update follow status in database (this will create the user if they don't exist) 53 | const success = await updateUserFollowStatus(username, userId); 54 | if (!success) { 55 | Logger.info(`Failed to update follow status for @${username} in database`); 56 | return { 57 | status: 'error', 58 | message: `Failed to update follow status for @${username} in database` 59 | }; 60 | } 61 | 62 | return { 63 | status: 'success', 64 | message: `Successfully followed user @${username}` 65 | }; 66 | } catch (error) { 67 | Logger.info('Error following user:', error); 68 | return { 69 | status: 'error', 70 | message: `Error following user @${username}`, 71 | error 72 | }; 73 | } 74 | } 75 | 76 | /** 77 | * Gets the Twitter user ID for a username 78 | * @param username - The username to look up 79 | * @returns Promise The user's Twitter ID or null if not found 80 | */ 81 | export async function getUserID(username: string): Promise { 82 | try { 83 | const userID = await scraper.getUserIdByScreenName(username); 84 | return userID || null; 85 | } catch (error) { 86 | Logger.info('Error getting user ID:', error); 87 | return null; 88 | } 89 | } -------------------------------------------------------------------------------- /src/utils/agentEventBus.ts: -------------------------------------------------------------------------------- 1 | // Manages communication between agents and the logger GUI 2 | import { Message } from '../types/agentSystem'; 3 | import { EventEmitter } from 'events'; 4 | 5 | interface AgentInfo { 6 | id: string; 7 | name: string; 8 | systemPrompt: string; 9 | chatHistory: Message[]; 10 | aiResponse: string | null; 11 | sessionStartTime: number; 12 | lastRunData?: any; 13 | } 14 | 15 | class AgentEventBus extends EventEmitter { 16 | private static instance: AgentEventBus; 17 | private activeAgents: Map = new Map(); 18 | private agentCounter = 0; 19 | 20 | private constructor() { 21 | super(); 22 | } 23 | 24 | static getInstance(): AgentEventBus { 25 | if (!AgentEventBus.instance) { 26 | AgentEventBus.instance = new AgentEventBus(); 27 | } 28 | return AgentEventBus.instance; 29 | } 30 | 31 | registerAgent(name: string = 'Agent'): string { 32 | this.agentCounter++; 33 | const id = `agent-${this.agentCounter}`; 34 | const sessionStartTime = Date.now(); 35 | 36 | const agentInfo: AgentInfo = { 37 | id, 38 | name: `${name} ${this.agentCounter}`, 39 | systemPrompt: '', 40 | chatHistory: [], 41 | aiResponse: null, 42 | sessionStartTime, 43 | lastRunData: null 44 | }; 45 | 46 | this.activeAgents.set(id, agentInfo); 47 | this.emit('newAgentSession', { agent: agentInfo }); 48 | return id; 49 | } 50 | 51 | getAgent(agentId: string): AgentInfo | undefined { 52 | return this.activeAgents.get(agentId); 53 | } 54 | 55 | getAllAgents(): AgentInfo[] { 56 | return Array.from(this.activeAgents.values()) 57 | .sort((a, b) => b.sessionStartTime - a.sessionStartTime); 58 | } 59 | 60 | updateAgentSystemPrompt(agentId: string, prompt: string) { 61 | const agent = this.activeAgents.get(agentId); 62 | if (agent) { 63 | agent.systemPrompt = prompt; 64 | this.emit('systemPromptUpdated', { agentId, prompt }); 65 | } 66 | } 67 | 68 | updateAgentChatHistory(agentId: string, messages: Message[]) { 69 | const agent = this.activeAgents.get(agentId); 70 | if (agent) { 71 | agent.chatHistory = [...messages]; 72 | this.emit('chatHistoryUpdated', { agentId, messages: agent.chatHistory }); 73 | } 74 | } 75 | 76 | updateAgentResponse(agentId: string, response: string) { 77 | const agent = this.activeAgents.get(agentId); 78 | if (agent) { 79 | agent.aiResponse = response; 80 | this.emit('aiResponseUpdated', { agentId, response }); 81 | } 82 | } 83 | 84 | updateAgentLastRunData(agentId: string, data: any) { 85 | const agent = this.activeAgents.get(agentId); 86 | if (agent) { 87 | agent.lastRunData = data; 88 | this.syncAgentChatHistory(agentId, data); 89 | this.emit('agentLastRunDataUpdated', { agentId, data }); 90 | } 91 | } 92 | 93 | syncAgentChatHistory(agentId: string, lastRunData: any) { 94 | if (lastRunData?.messageHistory) { 95 | this.updateAgentChatHistory(agentId, lastRunData.messageHistory); 96 | } 97 | } 98 | } 99 | 100 | export const agentEventBus = AgentEventBus.getInstance(); -------------------------------------------------------------------------------- /src/supabase/functions/terminal/terminalHistory.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../../../types/agentSystem'; 2 | import { Logger } from '../../../utils/logger'; 3 | import { supabase } from '../../supabaseClient'; 4 | 5 | type ValidRole = 'user' | 'assistant' | 'system'; 6 | 7 | export async function storeTerminalMessage( 8 | message: Message, 9 | sessionId: string 10 | ): Promise { 11 | try { 12 | if (message.role === 'function') { 13 | Logger.info('Skipping function message, not storing in history'); 14 | return; 15 | } 16 | 17 | const { error } = await supabase 18 | .from('short_term_terminal_history') 19 | .insert({ 20 | role: message.role as ValidRole, 21 | content: message.content || '', 22 | session_id: sessionId 23 | }); 24 | 25 | if (error) { 26 | Logger.error('Error storing terminal message:', error); 27 | throw error; 28 | } 29 | } catch (error) { 30 | Logger.error('Failed to store terminal message:', error); 31 | throw error; 32 | } 33 | } 34 | 35 | export async function getShortTermHistory(limit: number = 10): Promise { 36 | try { 37 | const { data, error } = await supabase 38 | .from('short_term_terminal_history') 39 | .select('*') 40 | .order('created_at', { ascending: false }) 41 | .limit(limit); 42 | 43 | if (error) { 44 | Logger.error('Error loading short term history:', error); 45 | throw error; 46 | } 47 | 48 | return data 49 | .map(entry => ({ 50 | role: entry.role as Message['role'], 51 | content: entry.content 52 | })) 53 | .reverse(); 54 | } catch (error) { 55 | Logger.error('Failed to load short term history:', error); 56 | throw error; 57 | } 58 | } 59 | 60 | export async function clearShortTermHistory(): Promise { 61 | try { 62 | const { error } = await supabase 63 | .from('short_term_terminal_history') 64 | .delete() 65 | .gte('id', 0); 66 | 67 | if (error) { 68 | Logger.error('Error clearing short term history:', error); 69 | throw error; 70 | } 71 | } catch (error) { 72 | Logger.error('Failed to clear short term history:', error); 73 | throw error; 74 | } 75 | } 76 | 77 | export async function getFormattedRecentHistory(limit: number = 6): Promise { 78 | try { 79 | const { data, error } = await supabase 80 | .from('short_term_terminal_history') 81 | .select('*') 82 | .order('created_at', { ascending: false }) 83 | .limit(limit); 84 | 85 | if (error) { 86 | Logger.error('Error loading recent history:', error); 87 | throw error; 88 | } 89 | 90 | const recentHistory = data.reverse(); 91 | 92 | return recentHistory 93 | .map((entry, index) => { 94 | const separator = index === 0 ? '' : '\n-------------------\n'; 95 | const roleLabel = entry.role === 'assistant' ? '[YOU]' : 96 | entry.role === 'user' ? '[TERMINAL]' : 97 | `[${entry.role.toUpperCase()}]`; 98 | return `${separator}${roleLabel}:\n${entry.content}`; 99 | }) 100 | .join(''); 101 | } catch (error) { 102 | Logger.error('Failed to load formatted recent history:', error); 103 | throw error; 104 | } 105 | } -------------------------------------------------------------------------------- /src/twitter/twitterClient.ts: -------------------------------------------------------------------------------- 1 | // Twitter client connectivity and authentication 2 | 3 | import { Scraper } from 'goat-x'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import dotenv from 'dotenv'; 7 | 8 | dotenv.config(); 9 | 10 | // Initialize the Scraper instance for interacting with Twitter 11 | export const scraper = new Scraper(); 12 | 13 | // Function to log in and save cookies 14 | export async function loginAndSaveCookies() { 15 | console.log("Attempting to authenticate with Twitter..."); 16 | try { 17 | // Log in using credentials from environment variables 18 | await scraper.login( 19 | process.env.TWITTER_USERNAME!, 20 | process.env.TWITTER_PASSWORD!, 21 | process.env.TWITTER_EMAIL 22 | ); 23 | 24 | // Retrieve the current session cookies 25 | const cookies = await scraper.getCookies(); 26 | 27 | // Create directory if it doesn't exist 28 | const cookiesDir = path.dirname(path.resolve(__dirname, 'cookies.json')); 29 | if (!fs.existsSync(cookiesDir)) { 30 | fs.mkdirSync(cookiesDir, { recursive: true }); 31 | } 32 | 33 | // Save the cookies to a JSON file for future sessions 34 | fs.writeFileSync( 35 | path.resolve(__dirname, 'cookies.json'), 36 | JSON.stringify(cookies) 37 | ); 38 | 39 | console.log('Successfully authenticated and saved session.'); 40 | } catch (error) { 41 | console.error('Authentication failed:', error); 42 | throw new Error('Failed to authenticate with Twitter. Please check your credentials.'); 43 | } 44 | } 45 | 46 | // Function to load cookies from the JSON file 47 | export async function loadCookies() { 48 | const cookiesPath = path.resolve(__dirname, 'cookies.json'); 49 | 50 | // Check if cookies file exists 51 | if (!fs.existsSync(cookiesPath)) { 52 | // Return silently - will trigger new login 53 | return false; 54 | } 55 | 56 | try { 57 | // Read cookies from the file system 58 | const cookiesData = fs.readFileSync(cookiesPath, 'utf8'); 59 | const cookiesArray = JSON.parse(cookiesData); 60 | 61 | // Map cookies to the correct format (strings) 62 | const cookieStrings = cookiesArray.map((cookie: any) => { 63 | return `${cookie.key}=${cookie.value}; Domain=${cookie.domain}; Path=${cookie.path}; ${ 64 | cookie.secure ? 'Secure' : '' 65 | }; ${cookie.httpOnly ? 'HttpOnly' : ''}; SameSite=${ 66 | cookie.sameSite || 'Lax' 67 | }`; 68 | }); 69 | 70 | // Set the cookies for the current session 71 | await scraper.setCookies(cookieStrings); 72 | return true; 73 | } catch (error) { 74 | // If there's an error reading/parsing cookies, delete the corrupt file 75 | try { 76 | fs.unlinkSync(cookiesPath); 77 | } catch (e) { 78 | // Ignore deletion errors 79 | } 80 | return false; 81 | } 82 | } 83 | 84 | // Function to ensure the scraper is authenticated 85 | export async function ensureAuthenticated() { 86 | try { 87 | // Attempt to load cookies from cookies.json 88 | const cookiesLoaded = await loadCookies(); 89 | 90 | // Check if the scraper is logged in 91 | const loggedIn = cookiesLoaded && await scraper.isLoggedIn(); 92 | 93 | if (loggedIn) { 94 | // Successfully authenticated with existing cookies 95 | return true; 96 | } else { 97 | // If not logged in or no cookies, perform fresh login 98 | await loginAndSaveCookies(); 99 | return true; 100 | } 101 | } catch (error) { 102 | throw new Error(`Twitter authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); 103 | } 104 | } -------------------------------------------------------------------------------- /src/supabase/functions/twitter/tweetInteractionChecks.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | 4 | type TweetAction = 'reply' | 'quote' | 'retweet'; 5 | 6 | export async function hasAlreadyActioned( 7 | tweetId: string, 8 | action: TweetAction 9 | ): Promise { 10 | try { 11 | const field = action === 'reply' ? 'in_reply_to_tweet_id' : 12 | action === 'quote' ? 'quoted_tweet_id' : 13 | 'retweeted_tweet_id'; 14 | 15 | const { data, error } = await supabase 16 | .from('twitter_tweets') 17 | .select('id') 18 | .eq(field, tweetId) 19 | .maybeSingle(); 20 | 21 | if (error) { 22 | Logger.error('Error checking tweet action status:', error.message); 23 | return false; 24 | } 25 | 26 | const hasActioned = !!data; 27 | Logger.info(`Tweet ${tweetId} ${action} status: ${hasActioned ? 'already done' : 'not done yet'}`); 28 | return hasActioned; 29 | 30 | } catch (error) { 31 | Logger.error(`Error checking if tweet ${tweetId} was already ${action}ed:`, error); 32 | return false; 33 | } 34 | } 35 | 36 | export async function hasInteractedWithTweet(tweetId: string): Promise { 37 | try { 38 | Logger.info(`Checking interactions for tweet ${tweetId}...`); 39 | 40 | const { data: interactions, error } = await supabase 41 | .from('twitter_tweets') 42 | .select('tweet_type, in_reply_to_tweet_id, quoted_tweet_id, retweeted_tweet_id') 43 | .or( 44 | `in_reply_to_tweet_id.eq.${tweetId},` + 45 | `quoted_tweet_id.eq.${tweetId},` + 46 | `retweeted_tweet_id.eq.${tweetId}` 47 | ); 48 | 49 | if (error) { 50 | Logger.error('Error checking tweet interactions:', error); 51 | return false; 52 | } 53 | 54 | if (interactions && interactions.length > 0) { 55 | const interactionTypes = interactions.map(i => { 56 | if (i.in_reply_to_tweet_id === tweetId) return 'reply'; 57 | if (i.quoted_tweet_id === tweetId) return 'quote'; 58 | if (i.retweeted_tweet_id === tweetId) return 'retweet'; 59 | return null; 60 | }).filter(Boolean); 61 | 62 | Logger.info(`Found previous interactions for tweet ${tweetId}:`, interactionTypes); 63 | return true; 64 | } 65 | 66 | Logger.info(`No previous interactions found for tweet ${tweetId}`); 67 | return false; 68 | 69 | } catch (error) { 70 | Logger.error(`Error checking tweet interactions for ${tweetId}:`, error); 71 | return false; 72 | } 73 | } 74 | 75 | export async function debugTweetInteractions(tweetId: string): Promise { 76 | try { 77 | const { data, error } = await supabase 78 | .from('twitter_tweets') 79 | .select('*') 80 | .or( 81 | `in_reply_to_tweet_id.eq.${tweetId},` + 82 | `quoted_tweet_id.eq.${tweetId},` + 83 | `retweeted_tweet_id.eq.${tweetId}` 84 | ); 85 | 86 | if (error) { 87 | Logger.error('Error in debug check:', error); 88 | return; 89 | } 90 | 91 | Logger.info(`Debug: Found ${data.length} interactions for tweet ${tweetId}:`); 92 | data.forEach(interaction => { 93 | if (interaction.in_reply_to_tweet_id === tweetId) { 94 | Logger.info(`- Reply (Tweet ID: ${interaction.tweet_id})`); 95 | } 96 | if (interaction.quoted_tweet_id === tweetId) { 97 | Logger.info(`- Quote (Tweet ID: ${interaction.tweet_id})`); 98 | } 99 | if (interaction.retweeted_tweet_id === tweetId) { 100 | Logger.info(`- Retweet (Tweet ID: ${interaction.tweet_id})`); 101 | } 102 | }); 103 | } catch (error) { 104 | Logger.error('Debug error:', error); 105 | } 106 | } -------------------------------------------------------------------------------- /src/twitter/utils/mediaUtils.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { URL } from 'url'; 3 | import { type FileTypeResult } from 'file-type'; 4 | 5 | // Initialize fileType module 6 | let fileTypeFromBuffer: ((buffer: Buffer) => Promise) | undefined; 7 | 8 | // Initialize the module 9 | (async () => { 10 | const fileType = await import('file-type'); 11 | fileTypeFromBuffer = fileType.fileTypeFromBuffer; 12 | })().catch(console.error); 13 | 14 | /** 15 | * Determines the media type based on URL or file extension 16 | * @param url - The media URL or file path 17 | * @returns The corresponding MIME type string 18 | */ 19 | export function getMediaType(url: string): string { 20 | try { 21 | // First try to get content type from URL extension 22 | const ext = new URL(url).pathname.split('.').pop()?.toLowerCase(); 23 | 24 | // Map common extensions to proper MIME types 25 | const mimeTypes: Record = { 26 | 'jpg': 'image/jpeg', 27 | 'jpeg': 'image/jpeg', 28 | 'png': 'image/png', 29 | 'gif': 'image/gif', 30 | 'mp4': 'video/mp4', 31 | 'webp': 'image/webp', 32 | 'webm': 'video/webm' 33 | }; 34 | 35 | // Return proper MIME type if extension is recognized 36 | if (ext && ext in mimeTypes) { 37 | return mimeTypes[ext]; 38 | } 39 | 40 | // If no extension or unrecognized, try to detect from response headers 41 | return 'application/octet-stream'; // Fallback type 42 | } catch (error) { 43 | console.error('Error determining media type:', error); 44 | return 'application/octet-stream'; 45 | } 46 | } 47 | 48 | /** 49 | * Fetches media content from URL and prepares it for tweet attachment 50 | * @param url - URL of the media file 51 | * @returns Promise resolving to media data object 52 | */ 53 | async function fetchMediaFromUrl(url: string): Promise<{ data: Buffer; mediaType: string }> { 54 | try { 55 | const response = await fetch(url); 56 | if (!response.ok) { 57 | throw new Error(`HTTP error! status: ${response.status}`); 58 | } 59 | 60 | // Read the response buffer 61 | const buffer = Buffer.from(await response.arrayBuffer()); 62 | 63 | // Get content type from response headers 64 | let contentType = response.headers.get('content-type'); 65 | 66 | // If content-type is missing or generic, detect it from buffer 67 | if (!contentType || contentType === 'application/octet-stream') { 68 | if (fileTypeFromBuffer) { 69 | const fileTypeResult = await fileTypeFromBuffer(buffer); 70 | contentType = fileTypeResult ? fileTypeResult.mime : 'application/octet-stream'; 71 | } else { 72 | contentType = 'application/octet-stream'; 73 | } 74 | } 75 | 76 | return { 77 | data: buffer, 78 | mediaType: contentType 79 | }; 80 | } catch (error) { 81 | console.error(`Error fetching media from URL ${url}:`, error); 82 | throw error; 83 | } 84 | } 85 | 86 | /** 87 | * Prepares media data for tweet attachments from URLs 88 | * @param mediaUrls - Array of media URLs (images, GIFs, or videos) 89 | * @returns Promise resolving to array of media data objects 90 | */ 91 | export async function prepareMediaData(mediaUrls: string[]) { 92 | if (!mediaUrls || mediaUrls.length === 0) return undefined; 93 | 94 | // Validate URLs 95 | mediaUrls.forEach(url => { 96 | try { 97 | new URL(url); 98 | } catch (error) { 99 | throw new Error(`Invalid URL: ${url}`); 100 | } 101 | }); 102 | 103 | try { 104 | return await Promise.all(mediaUrls.map(fetchMediaFromUrl)); 105 | } catch (error) { 106 | console.error('Error preparing media data:', error); 107 | throw error; 108 | } 109 | } -------------------------------------------------------------------------------- /mainTests.ts: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | import { spawn, ChildProcess } from 'child_process'; 3 | 4 | // A mapping of test names to their file paths 5 | const tests = { 6 | '1': { name: 'Terminal Loop Test', path: 'src/tests/terminalLoop.ts' }, 7 | '2': { name: 'Chat Room Test', path: 'src/tests/chatRoomTest.ts' }, 8 | '3': { name: 'Tool Calling Test', path: 'src/tests/toolCalling.ts' }, 9 | '4': { name: 'Infinite Backrooms Test', path: 'src/tests/infiniteBackrooms.ts' }, 10 | '5': { name: 'Structured Output Test', path: 'src/tests/structuredOutput.ts' } 11 | }; 12 | 13 | // Track current running test process 14 | let currentTestProcess: ChildProcess | null = null; 15 | // Track if test was interrupted 16 | let wasTestInterrupted = false; 17 | 18 | function showMenu() { 19 | console.log('\nAvailable Tests:'); 20 | for (const [key, test] of Object.entries(tests)) { 21 | console.log(`${key}. ${test.name}`); 22 | } 23 | console.log('\nSelect a test number to run it, or type "exit" to quit:'); 24 | } 25 | 26 | function cleanExit() { 27 | if (currentTestProcess) { 28 | currentTestProcess.kill('SIGINT'); 29 | } 30 | console.log('\nGoodbye!'); 31 | process.exit(0); 32 | } 33 | 34 | // Handle SIGINT (Ctrl+C) for the main process 35 | function setupMainProcessHandlers(rl: readline.Interface) { 36 | process.on('SIGINT', () => { 37 | if (currentTestProcess) { 38 | // If a test is running, kill it and return to menu 39 | wasTestInterrupted = true; 40 | currentTestProcess.kill('SIGINT'); 41 | currentTestProcess = null; 42 | console.log('\nTest interrupted. Returning to menu...'); 43 | } else { 44 | // If no test is running, confirm exit 45 | console.log('\nAre you sure you want to exit? (y/n)'); 46 | rl.question('', (answer) => { 47 | if (answer.toLowerCase() === 'y') { 48 | cleanExit(); 49 | } else { 50 | showMenu(); 51 | } 52 | }); 53 | } 54 | }); 55 | 56 | // Handle normal process exit 57 | process.on('exit', () => { 58 | if (currentTestProcess) { 59 | currentTestProcess.kill('SIGINT'); 60 | } 61 | }); 62 | } 63 | 64 | async function main() { 65 | const rl = readline.createInterface({ 66 | input: process.stdin, 67 | output: process.stdout, 68 | }); 69 | 70 | // Setup SIGINT handlers 71 | setupMainProcessHandlers(rl); 72 | 73 | showMenu(); 74 | 75 | rl.on('line', (input: string) => { 76 | const trimmed = input.trim(); 77 | if (trimmed.toLowerCase() === 'exit') { 78 | cleanExit(); 79 | } else if (tests[trimmed]) { 80 | const test = tests[trimmed]; 81 | console.log(`\nRunning "${test.name}"...\n`); 82 | 83 | // Reset interrupted flag 84 | wasTestInterrupted = false; 85 | 86 | // Spawn a child process to run the selected test with ts-node 87 | currentTestProcess = spawn('npx', ['ts-node', test.path], { stdio: 'inherit' }); 88 | 89 | currentTestProcess.on('close', (code) => { 90 | if (!wasTestInterrupted) { 91 | console.log(`\n"${test.name}" ${code === null ? 'was interrupted' : `exited with code ${code}`}.`); 92 | showMenu(); 93 | } 94 | currentTestProcess = null; 95 | }); 96 | 97 | // Handle test process errors 98 | currentTestProcess.on('error', (err) => { 99 | console.error(`Error running test: ${err.message}`); 100 | currentTestProcess = null; 101 | showMenu(); 102 | }); 103 | } else { 104 | console.log('Invalid selection. Please try again.'); 105 | showMenu(); 106 | } 107 | }); 108 | } 109 | 110 | main().catch((err) => { 111 | console.error('Error running CLI:', err); 112 | process.exit(1); 113 | }); -------------------------------------------------------------------------------- /src/models/adapters/AnthropicAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Message, Tool } from '../../types/agentSystem'; 2 | import { ModelAdapter, ProcessedResponse, FunctionCall } from './ModelAdapter'; 3 | import { Logger } from '../../utils/logger'; 4 | 5 | export class AnthropicAdapter extends ModelAdapter { 6 | public buildParams( 7 | messages: Message[], 8 | tools: Tool[], 9 | toolChoice?: any, 10 | systemPrompt?: string, 11 | outputSchema?: any 12 | ): any { 13 | let systemMsg = ''; 14 | let nonSystemMessages = messages.filter(m => m.role !== 'system'); 15 | const finalMessages = nonSystemMessages.map(m => { 16 | let role = m.role; 17 | if (role === 'system') role = 'user'; 18 | 19 | const contentArr = m.content ? [{type:"text",text:m.content}] : []; 20 | return { role, content: contentArr }; 21 | }); 22 | 23 | // If there are no messages after filtering, Anthropic requires at least one. 24 | // Add a dummy user message if needed. 25 | if (finalMessages.length === 0) { 26 | finalMessages.push({ 27 | role: 'user', 28 | content: [{ type: 'text', text: '.' }] 29 | }); 30 | } 31 | 32 | if (systemPrompt) { 33 | systemMsg = systemPrompt; 34 | } 35 | 36 | const params: any = { 37 | model: process.env.ANTHROPIC_MODEL || 'claude-3-5-sonnet-20241022', 38 | max_tokens: 1024, 39 | temperature: 0, 40 | system: systemMsg, 41 | messages: finalMessages 42 | }; 43 | 44 | let hasTools = false; 45 | if (tools && tools.length > 0) { 46 | params.tools = tools; 47 | hasTools = true; 48 | } 49 | 50 | if (hasTools) { 51 | if (toolChoice) { 52 | if (typeof toolChoice === 'string') { 53 | params.tool_choice = { type: toolChoice }; 54 | } else { 55 | params.tool_choice = toolChoice; 56 | } 57 | } else { 58 | params.tool_choice = { type: "auto" }; 59 | } 60 | } 61 | 62 | return params; 63 | } 64 | 65 | public formatTools(tools: Tool[]): any[] { 66 | const processed = tools.map(tool => { 67 | if (!tool.function || !tool.function.name || !tool.function.parameters || !tool.function.description) { 68 | Logger.error('[AnthropicAdapter] Tool missing function fields in formatTools:', tool); 69 | return null; 70 | } 71 | return { 72 | name: tool.function.name, 73 | description: tool.function.description, 74 | input_schema: tool.function.parameters 75 | }; 76 | }).filter(Boolean); 77 | return processed; 78 | } 79 | 80 | public buildToolChoice(tools: Tool[]): any { 81 | if (tools && tools.length > 0) { 82 | return { type: "auto" }; 83 | } 84 | return undefined; 85 | } 86 | 87 | public processResponse(response: any): ProcessedResponse { 88 | if (!response || response.type !== 'message') { 89 | Logger.error('[AnthropicAdapter] Invalid response format:', response); 90 | return { functionCalls: [] }; 91 | } 92 | 93 | const message = response; 94 | const contentBlocks = Array.isArray(message.content) ? message.content : []; 95 | 96 | const toolBlocks = contentBlocks.filter((c: any) => c.type === 'tool_use'); 97 | const functionCalls: FunctionCall[] = toolBlocks.map((tb: any) => ({ 98 | functionName: tb.name, 99 | functionArgs: tb.input || {} 100 | })); 101 | 102 | const textBlocks = contentBlocks 103 | .filter((c: any) => c.type === 'text') 104 | .map((c: any) => c.text) 105 | .join(''); 106 | 107 | let aiMessage; 108 | if (textBlocks && message.role === 'assistant') { 109 | aiMessage = { role: 'assistant', content: textBlocks }; 110 | } 111 | 112 | return { aiMessage, functionCalls }; 113 | } 114 | } -------------------------------------------------------------------------------- /src/twitter/functions/retweet.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { likeTweet } from './likeTweet'; 3 | import { analyzeTweetContext } from '../utils/tweetUtils'; 4 | import { findOrCreateUserFromTweet } from '../utils/profileUtils'; 5 | import { Logger } from '../../utils/logger'; 6 | import { logTweet } from '../../supabase/functions/twitter/tweetEntries'; 7 | import { logTwitterInteraction } from '../../supabase/functions/twitter/interactionEntries'; 8 | import { hasAlreadyActioned } from '../../supabase/functions/twitter/tweetInteractionChecks'; 9 | import { RetweetResult } from '../types/tweetResults'; 10 | 11 | /** 12 | * Retweets a specific tweet 13 | * @param tweetId - The ID of the tweet to retweet 14 | * @returns Promise with status and details 15 | */ 16 | export async function retweet(tweetId: string): Promise { 17 | try { 18 | // Check if already retweeted 19 | Logger.info(`Checking if tweet ${tweetId} was already retweeted...`); 20 | const hasRetweeted = await hasAlreadyActioned(tweetId, 'retweet'); 21 | 22 | if (hasRetweeted) { 23 | const message = `Already retweeted tweet ${tweetId}`; 24 | Logger.info(message); 25 | return { 26 | success: false, 27 | message 28 | }; 29 | } 30 | 31 | // Fetch the tweet we're retweeting 32 | const targetTweet = await scraper.getTweet(tweetId); 33 | if (!targetTweet || !targetTweet.username) { 34 | const message = 'Failed to fetch target tweet'; 35 | Logger.error(message); 36 | return { 37 | success: false, 38 | message 39 | }; 40 | } 41 | 42 | // Like the tweet before retweeting 43 | await likeTweet(tweetId); 44 | 45 | try { 46 | // Attempt to retweet 47 | await scraper.retweet(tweetId); 48 | } catch (error) { 49 | const message = `Failed to retweet: ${error}`; 50 | Logger.error(message); 51 | return { 52 | success: false, 53 | message 54 | }; 55 | } 56 | 57 | Logger.info(`Successfully retweeted tweet ${tweetId}`); 58 | 59 | // Generate a unique composite ID for the retweet entry 60 | const uniqueRetweetId = `rt_${tweetId}`; 61 | 62 | // Log the bot's retweet in the database with the unique composite ID 63 | const logResult = await logTweet({ 64 | tweet_id: uniqueRetweetId, // Use composite ID instead of null 65 | text: targetTweet.text || '', 66 | tweet_type: 'retweet', 67 | retweeted_tweet_id: tweetId, 68 | created_at: new Date().toISOString(), 69 | }); 70 | 71 | if (!logResult) { 72 | Logger.info('Warning: Failed to log retweet in database'); 73 | } 74 | 75 | // Find or create user account 76 | const userAccounts = await findOrCreateUserFromTweet(targetTweet); 77 | if (!userAccounts) { 78 | const message = 'Failed to process user account'; 79 | Logger.error(message); 80 | return { 81 | success: false, 82 | message 83 | }; 84 | } 85 | 86 | // Analyze the context of the tweet for logging 87 | const context = await analyzeTweetContext(targetTweet); 88 | 89 | // Log the interaction with the user 90 | await logTwitterInteraction({ 91 | tweetId: tweetId, 92 | userTweetText: targetTweet.text || '', 93 | userTweetTimestamp: targetTweet.timeParsed?.toISOString() || new Date().toISOString(), 94 | userId: userAccounts.userId || '', 95 | context, 96 | }); 97 | 98 | return { 99 | success: true, 100 | message: 'Successfully retweeted tweet' 101 | }; 102 | 103 | } catch (error) { 104 | const message = `Error retweeting tweet: ${error}`; 105 | Logger.info(message); 106 | return { 107 | success: false, 108 | message 109 | }; 110 | } 111 | } -------------------------------------------------------------------------------- /src/twitter/utils/profileUtils.ts: -------------------------------------------------------------------------------- 1 | import { Profile, Tweet } from 'goat-x'; 2 | import { Logger } from '../../utils/logger'; 3 | import { scraper } from '../twitterClient'; 4 | import { findTwitterUserByTwitterId, createTwitterUser } from '../../supabase/functions/twitter/userEntries'; 5 | 6 | /** 7 | * Converts Twitter Profile data to JSON-safe format 8 | * Handles Date conversions and any other non-JSON-safe types 9 | */ 10 | export function sanitizeProfileForJson(profile: Partial): Record { 11 | return { 12 | ...profile, 13 | // Convert Date to ISO string 14 | joined: profile.joined?.toISOString(), 15 | // Add any other Date field conversions here 16 | }; 17 | } 18 | 19 | /** 20 | * Gets comprehensive Twitter user info including full profile 21 | */ 22 | export async function getTwitterUserInfo(username: string): Promise<{ 23 | userId: string; 24 | username: string; 25 | profile: Partial; 26 | } | null> { 27 | try { 28 | const userId = await scraper.getUserIdByScreenName(username); 29 | const profile = await scraper.getProfile(username); 30 | 31 | return { 32 | userId, 33 | username: profile.username || username, 34 | profile: { 35 | avatar: profile.avatar, 36 | banner: profile.banner, 37 | biography: profile.biography, 38 | birthday: profile.birthday, 39 | followersCount: profile.followersCount, 40 | followingCount: profile.followingCount, 41 | friendsCount: profile.friendsCount, 42 | mediaCount: profile.mediaCount, 43 | statusesCount: profile.statusesCount, 44 | isPrivate: profile.isPrivate, 45 | isVerified: profile.isVerified, 46 | isBlueVerified: profile.isBlueVerified, 47 | joined: profile.joined, 48 | likesCount: profile.likesCount, 49 | listedCount: profile.listedCount, 50 | location: profile.location, 51 | name: profile.name, 52 | pinnedTweetIds: profile.pinnedTweetIds, 53 | tweetsCount: profile.tweetsCount, 54 | url: profile.url, 55 | website: profile.website, 56 | canDm: profile.canDm 57 | } 58 | }; 59 | } catch (error) { 60 | Logger.error('Error getting Twitter user info:', error); 61 | return null; 62 | } 63 | } 64 | 65 | /** 66 | * Utility function to find or create a user from a tweet 67 | * Returns user account info if successful, null if failed 68 | */ 69 | export async function findOrCreateUserFromTweet(tweet: Tweet): Promise<{ 70 | userAccountId: number; 71 | userId: string | null; 72 | } | null> { 73 | if (!tweet.username || !tweet.userId) { 74 | Logger.info('Tweet missing required user information'); 75 | return null; 76 | } 77 | 78 | // First check if user exists in database 79 | let userAccounts = await findTwitterUserByTwitterId(tweet.userId); 80 | 81 | if (userAccounts) { 82 | Logger.info(`Found existing user in database: @${tweet.username} (${tweet.userId})`); 83 | return userAccounts; 84 | } 85 | 86 | Logger.info(`New user detected: @${tweet.username} (${tweet.userId}). Fetching profile info...`); 87 | 88 | // If user not found, get their profile info and create them 89 | const userInfo = await getTwitterUserInfo(tweet.username); 90 | if (!userInfo) { 91 | Logger.error(`Failed to get Twitter profile info for new user: @${tweet.username}`); 92 | return null; 93 | } 94 | 95 | userAccounts = await createTwitterUser( 96 | userInfo.username, 97 | userInfo.userId, 98 | userInfo.profile 99 | ); 100 | 101 | if (!userAccounts) { 102 | Logger.error(`Failed to create new user account for: @${tweet.username}`); 103 | return null; 104 | } 105 | 106 | Logger.info(`Successfully created new user account for: @${tweet.username} (${tweet.userId})`); 107 | return userAccounts; 108 | } -------------------------------------------------------------------------------- /src/supabase/functions/twitter/userEntries.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | import { Profile } from 'goat-x'; 4 | 5 | function sanitizeProfileForJson(profile: Partial): Record { 6 | return { 7 | ...profile, 8 | joined: profile.joined?.toISOString(), 9 | }; 10 | } 11 | 12 | type TwitterUserResult = { 13 | userAccountId: number; 14 | userId: string | null; 15 | } | null; 16 | 17 | export async function findTwitterUserByTwitterId( 18 | twitterId: string 19 | ): Promise { 20 | try { 21 | const { data: existingAccount } = await supabase 22 | .from('user_accounts') 23 | .select('id, user_id') 24 | .eq('platform', 'twitter') 25 | .eq('platform_user_id', twitterId) 26 | .single(); 27 | 28 | if (!existingAccount) { 29 | return null; 30 | } 31 | 32 | return { 33 | userAccountId: existingAccount.id, 34 | userId: existingAccount.user_id 35 | }; 36 | } catch (error) { 37 | Logger.error('Error in findTwitterUser:', error); 38 | return null; 39 | } 40 | } 41 | 42 | export async function findTwitterUserByUsername( 43 | username: string 44 | ): Promise { 45 | try { 46 | const { data: existingAccount } = await supabase 47 | .from('user_accounts') 48 | .select('id, user_id') 49 | .eq('platform', 'twitter') 50 | .eq('username', username) 51 | .single(); 52 | 53 | if (!existingAccount) { 54 | return null; 55 | } 56 | 57 | return { 58 | userAccountId: existingAccount.id, 59 | userId: existingAccount.user_id 60 | }; 61 | } catch (error) { 62 | Logger.error('Error in findTwitterUserByUsername:', error); 63 | return null; 64 | } 65 | } 66 | 67 | export async function updateTwitterUserProfile( 68 | userAccountId: number, 69 | profileData: Partial 70 | ): Promise { 71 | try { 72 | const { error } = await supabase 73 | .from('twitter_user_accounts') 74 | .update({ 75 | profile_data: sanitizeProfileForJson(profileData), 76 | last_profile_update: new Date().toISOString() 77 | }) 78 | .eq('user_account_id', userAccountId); 79 | 80 | return !error; 81 | } catch (error) { 82 | Logger.error('Error updating Twitter user profile:', error); 83 | return false; 84 | } 85 | } 86 | 87 | export async function createTwitterUser( 88 | username: string, 89 | twitterId: string, 90 | profileData?: Partial 91 | ): Promise { 92 | try { 93 | const { data: newUser } = await supabase 94 | .from('users') 95 | .insert({ 96 | created_at: new Date().toISOString(), 97 | updated_at: new Date().toISOString() 98 | }) 99 | .select('id') 100 | .single(); 101 | 102 | if (!newUser) { 103 | Logger.error('Failed to create new user'); 104 | return null; 105 | } 106 | 107 | const { data: newAccount } = await supabase 108 | .from('user_accounts') 109 | .insert({ 110 | user_id: newUser.id, 111 | platform: 'twitter', 112 | platform_user_id: twitterId, 113 | username: username, 114 | connected_at: new Date().toISOString() 115 | }) 116 | .select('id') 117 | .single(); 118 | 119 | if (!newAccount) { 120 | Logger.error('Failed to create user account'); 121 | return null; 122 | } 123 | 124 | const { error: twitterError } = await supabase 125 | .from('twitter_user_accounts') 126 | .insert({ 127 | user_account_id: newAccount.id, 128 | is_followed_by_bot: null, 129 | profile_data: profileData ? sanitizeProfileForJson(profileData) : null, 130 | last_profile_update: new Date().toISOString() 131 | }); 132 | 133 | if (twitterError) { 134 | Logger.error('Error creating twitter user account:', twitterError); 135 | return null; 136 | } 137 | 138 | return { 139 | userAccountId: newAccount.id, 140 | userId: newUser.id 141 | }; 142 | } catch (error) { 143 | Logger.error('Error in createTwitterUser:', error); 144 | return null; 145 | } 146 | } -------------------------------------------------------------------------------- /src/supabase/functions/twitter/tweetEntries.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { uploadAndLogMedia } from './mediaEntries'; 3 | import { Logger } from '../../../utils/logger'; 4 | import { formatTimestamp } from '../../../utils/formatTimestamps'; 5 | 6 | interface TweetData { 7 | tweet_id?: string | null; 8 | text: string; 9 | tweet_type: 'main' | 'reply' | 'quote' | 'retweet'; 10 | has_media?: boolean; 11 | bot_username?: string; 12 | in_reply_to_tweet_id?: string; 13 | retweeted_tweet_id?: string; 14 | quoted_tweet_id?: string; 15 | created_at?: string; 16 | } 17 | 18 | export async function logTweet( 19 | data: TweetData, 20 | mediaData?: { data: Buffer; mediaType: string; }[] 21 | ): Promise { 22 | try { 23 | let mediaIds: string[] | null = null; 24 | 25 | if (mediaData && mediaData.length > 0 && data.tweet_id) { 26 | const mediaIdResults = await Promise.all( 27 | mediaData.map(async ({ data: mediaBuffer, mediaType }) => { 28 | try { 29 | return await uploadAndLogMedia(mediaBuffer, data.tweet_id!, mediaType); 30 | } catch (error) { 31 | Logger.error('Error processing media:', error); 32 | return null; 33 | } 34 | }) 35 | ); 36 | 37 | mediaIds = mediaIdResults.filter((id): id is string => id !== null); 38 | } 39 | 40 | const currentTime = new Date().toISOString(); 41 | const insertData = { 42 | tweet_id: data.tweet_id || (data.tweet_type === 'retweet' ? 43 | `rt_${data.retweeted_tweet_id}` : null), 44 | text: data.text.trim(), 45 | tweet_type: data.tweet_type, 46 | has_media: mediaData && mediaData.length > 0 ? true : false, 47 | bot_username: data.bot_username || process.env.TWITTER_USERNAME, 48 | in_reply_to_tweet_id: data.in_reply_to_tweet_id, 49 | retweeted_tweet_id: data.retweeted_tweet_id, 50 | quoted_tweet_id: data.quoted_tweet_id, 51 | created_at: currentTime, 52 | }; 53 | 54 | Logger.info(`Logging tweet with created_at: ${insertData.created_at}`); 55 | 56 | if (new Date(insertData.created_at) > new Date()) { 57 | Logger.info(`Error: Attempted to set created_at to a future time.`); 58 | insertData.created_at = new Date().toISOString(); 59 | } 60 | 61 | const { data: tweet, error } = await supabase 62 | .from('twitter_tweets') 63 | .insert(insertData) 64 | .select('tweet_id') 65 | .maybeSingle(); 66 | 67 | if (error) { 68 | Logger.error('Error logging tweet to Supabase:', error.message); 69 | Logger.error('Error details:', error.details); 70 | Logger.error('Error hint:', error.hint); 71 | return null; 72 | } 73 | 74 | if (mediaIds && mediaIds.length > 0 && data.tweet_id) { 75 | const mediaRelations = mediaIds.map((mediaId) => ({ 76 | tweet_id: data.tweet_id!, 77 | media_id: mediaId, 78 | })); 79 | 80 | const { error: mediaRelationError } = await supabase 81 | .from('tweet_media') 82 | .insert(mediaRelations); 83 | 84 | if (mediaRelationError) { 85 | Logger.error('Error creating media relations:', mediaRelationError); 86 | } 87 | } 88 | 89 | Logger.info('Successfully logged tweet to Supabase:', tweet); 90 | return tweet?.tweet_id || null; 91 | } catch (error) { 92 | Logger.error('Exception in logTweet:', error); 93 | return null; 94 | } 95 | } 96 | 97 | export async function getRecentMainTweets(): Promise { 98 | try { 99 | const { data: tweets, error } = await supabase 100 | .from('twitter_tweets') 101 | .select('*') 102 | .eq('tweet_type', 'main') 103 | .order('created_at', { ascending: false }) 104 | .limit(5); 105 | 106 | if (error) { 107 | Logger.error('Error fetching recent tweets:', error); 108 | return null; 109 | } 110 | 111 | if (!tweets || tweets.length === 0) { 112 | return "No recent main tweets found."; 113 | } 114 | 115 | const formattedTweets = tweets.map(tweet => { 116 | const timestamp = formatTimestamp(tweet.created_at || new Date().toISOString()); 117 | const mediaIndicator = tweet.has_media ? '[has media]' : '[no media]'; 118 | return `[${timestamp}] - ${tweet.text} ${mediaIndicator}`; 119 | }); 120 | 121 | return formattedTweets.reverse().join('\n'); 122 | } catch (error) { 123 | Logger.error('Exception in getRecentMainTweets:', error); 124 | return null; 125 | } 126 | } -------------------------------------------------------------------------------- /src/twitter/functions/quoteTweet.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { prepareMediaData } from '../utils/mediaUtils'; 3 | import { likeTweet } from './likeTweet'; 4 | import { analyzeTweetContext } from '../utils/tweetUtils'; 5 | import { findOrCreateUserFromTweet } from '../utils/profileUtils'; 6 | import { Logger } from '../../utils/logger'; 7 | import { logTweet } from '../../supabase/functions/twitter/tweetEntries'; 8 | import { logTwitterInteraction } from '../../supabase/functions/twitter/interactionEntries'; 9 | import { hasAlreadyActioned } from '../../supabase/functions/twitter/tweetInteractionChecks'; 10 | import { QuoteResult } from '../types/tweetResults'; 11 | 12 | // Add this interface near the top of the file 13 | interface TwitterResponse { 14 | data?: { 15 | create_tweet?: { 16 | tweet_results?: { 17 | result?: { 18 | rest_id?: string; 19 | }; 20 | }; 21 | }; 22 | }; 23 | } 24 | 25 | /** 26 | * Sends a quote tweet with optional media attachments 27 | * @param quotedTweetId - The ID of the tweet being quoted 28 | * @param text - The text content of the quote tweet 29 | * @param mediaUrls - Optional array of media URLs (images, GIFs, or videos) 30 | * @returns Promise with status and tweet ID if successful 31 | */ 32 | export async function quoteTweet( 33 | quotedTweetId: string, 34 | text: string, 35 | mediaUrls?: string[], 36 | twitterInterface?: string 37 | ): Promise { 38 | try { 39 | // Check if already quoted this tweet 40 | const hasQuoted = await hasAlreadyActioned(quotedTweetId, 'quote'); 41 | if (hasQuoted) { 42 | Logger.info(`Already quote tweeted ${quotedTweetId}`); 43 | return { 44 | success: false, 45 | message: 'Already quote tweeted this tweet' 46 | }; 47 | } 48 | 49 | // Fetch the tweet we're quoting 50 | const targetTweet = await scraper.getTweet(quotedTweetId); 51 | if (!targetTweet || !targetTweet.username) { 52 | Logger.info('Failed to fetch target tweet'); 53 | return { 54 | success: false, 55 | message: 'Failed to fetch target tweet' 56 | }; 57 | } 58 | 59 | // Prepare media data if any 60 | const mediaData = mediaUrls ? await prepareMediaData(mediaUrls) : undefined; 61 | 62 | // Like the tweet before quoting it 63 | await likeTweet(quotedTweetId); 64 | 65 | // Send the quote tweet 66 | const response = await scraper.sendQuoteTweet(text, quotedTweetId, { 67 | mediaData: mediaData || [], 68 | }); 69 | 70 | // Update the response typing 71 | const responseData = (await response.json()) as TwitterResponse; 72 | const tweetId = responseData?.data?.create_tweet?.tweet_results?.result?.rest_id; 73 | 74 | if (!tweetId) { 75 | Logger.error('Failed to retrieve tweet ID from response:', responseData); 76 | return { 77 | success: false, 78 | message: 'Failed to retrieve tweet ID from response' 79 | }; 80 | } 81 | 82 | // Log the bot's quote tweet in the database 83 | await logTweet({ 84 | tweet_id: tweetId, 85 | text: text, 86 | tweet_type: 'quote', 87 | has_media: !!mediaData, 88 | quoted_tweet_id: quotedTweetId, 89 | created_at: new Date().toISOString(), 90 | }, mediaData); 91 | 92 | // Find or create user account 93 | const userAccounts = await findOrCreateUserFromTweet(targetTweet); 94 | if (!userAccounts) { 95 | Logger.info('Failed to process user account'); 96 | return { 97 | success: false, 98 | message: 'Failed to process user account' 99 | }; 100 | } 101 | 102 | // Analyze tweet context 103 | const context = { 104 | ...(await analyzeTweetContext(targetTweet)), 105 | twitterInterface: twitterInterface 106 | }; 107 | 108 | // Log the interaction with the user 109 | await logTwitterInteraction({ 110 | tweetId: quotedTweetId, 111 | userTweetText: targetTweet.text || '', 112 | userTweetTimestamp: targetTweet.timeParsed?.toISOString() || new Date().toISOString(), 113 | userId: userAccounts.userId || '', 114 | context, 115 | }); 116 | 117 | Logger.info(`Quote tweet sent successfully (ID: ${tweetId})`); 118 | return { 119 | success: true, 120 | message: 'Successfully quote tweeted', 121 | tweetId: tweetId 122 | }; 123 | 124 | } catch (error) { 125 | Logger.info('Error sending quote tweet:', error); 126 | return { 127 | success: false, 128 | message: `Failed to quote tweet: ${error}` 129 | }; 130 | } 131 | } -------------------------------------------------------------------------------- /src/twitter/functions/getMentions.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import type { Tweet } from 'goat-x'; 3 | import { formatTimestamp } from '../../utils/formatTimestamps'; 4 | import { hasInteractedWithTweet, debugTweetInteractions } from '../../supabase/functions/twitter/tweetInteractionChecks'; 5 | import { isUserFollowedByBot } from '../../supabase/functions/twitter/followEntries'; 6 | import { Logger } from '../../utils/logger'; 7 | 8 | interface ExtendedTweet extends Tweet { 9 | originalTweetText?: string; 10 | originalTweetAuthor?: string; 11 | originalTweetId?: string; 12 | isFollowing?: boolean; 13 | } 14 | 15 | /** 16 | * Gets recent mentions of the bot's account 17 | * @param maxTweets - Maximum number of mentions to fetch (default: 20) 18 | * @returns Array of formatted mention strings 19 | */ 20 | export async function getMentions(maxTweets: number = 20): Promise { 21 | try { 22 | Logger.info('Fetching mentions...'); 23 | const rawMentions: Tweet[] = []; 24 | const query = `@${process.env.TWITTER_USERNAME}`; 25 | const searchMode = 1; // SearchMode.Latest 26 | 27 | // First just collect all raw mentions 28 | for await (const tweet of scraper.searchTweets(query, maxTweets, searchMode)) { 29 | if (tweet.username !== process.env.TWITTER_USERNAME && tweet.id) { 30 | rawMentions.push(tweet); 31 | } 32 | } 33 | 34 | Logger.info(`Found ${rawMentions.length} total mentions, checking for previous interactions...`); 35 | 36 | // Filter out already interacted tweets first 37 | const unhandledMentions = await Promise.all( 38 | rawMentions.map(async (tweet) => { 39 | const hasInteracted = await hasInteractedWithTweet(tweet.id!); 40 | if (hasInteracted) { 41 | await debugTweetInteractions(tweet.id!); 42 | Logger.info(`Filtering out tweet ${tweet.id} - already interacted with`); 43 | return null; 44 | } 45 | return tweet; 46 | }) 47 | ); 48 | 49 | // Process only unhandled mentions 50 | const processedMentions: ExtendedTweet[] = []; 51 | for (const tweet of unhandledMentions.filter((t): t is Tweet => t !== null)) { 52 | Logger.info(`Processing unhandled mention from @${tweet.username} (ID: ${tweet.id})`); 53 | const extendedTweet = tweet as ExtendedTweet; 54 | 55 | // Add following status 56 | if (extendedTweet.username) { 57 | const isFollowing = await isUserFollowedByBot(extendedTweet.username); 58 | extendedTweet.isFollowing = isFollowing; 59 | Logger.info(`Following status for @${extendedTweet.username}: ${isFollowing}`); 60 | } 61 | 62 | // Fetch original tweet if it's a reply 63 | if (extendedTweet.inReplyToStatusId) { 64 | try { 65 | Logger.info(`Fetching parent tweet ${extendedTweet.inReplyToStatusId}...`); 66 | await new Promise(resolve => setTimeout(resolve, 500)); 67 | const originalTweet = await scraper.getTweet(extendedTweet.inReplyToStatusId); 68 | if (originalTweet && originalTweet.text) { 69 | extendedTweet.originalTweetText = originalTweet.text; 70 | extendedTweet.originalTweetAuthor = originalTweet.username; 71 | extendedTweet.originalTweetId = originalTweet.id; 72 | Logger.info(`Found parent tweet from @${originalTweet.username}`); 73 | } 74 | } catch (error) { 75 | Logger.error(`Failed to fetch parent tweet: ${error}`); 76 | } 77 | } 78 | 79 | processedMentions.push(extendedTweet); 80 | } 81 | 82 | // Format the processed mentions 83 | const formattedMentions = processedMentions.map(mention => { 84 | const timestamp = mention.timeParsed ? 85 | formatTimestamp(new Date(mention.timeParsed)) : 86 | 'Unknown time'; 87 | 88 | let output = `- [${mention.id}] @${mention.username} (${mention.isFollowing ? 'Following' : 'Not following'}) (${timestamp}): ${mention.text}`; 89 | 90 | if (mention.originalTweetText && mention.originalTweetAuthor) { 91 | const isBot = mention.originalTweetAuthor === process.env.TWITTER_USERNAME; 92 | const authorDisplay = isBot ? `@${mention.originalTweetAuthor} (YOU)` : `@${mention.originalTweetAuthor}`; 93 | const tweetIdDisplay = mention.originalTweetId ? `[${mention.originalTweetId}]` : ''; 94 | 95 | output += `\n ↳ In reply to ${authorDisplay} ${tweetIdDisplay}: "${mention.originalTweetText}"`; 96 | } 97 | 98 | return output; 99 | }); 100 | 101 | Logger.info(`Returning ${formattedMentions.length} formatted mentions`); 102 | return formattedMentions; 103 | 104 | } catch (error) { 105 | Logger.error('Error fetching mentions:', error); 106 | return []; 107 | } 108 | } -------------------------------------------------------------------------------- /src/supabase/functions/twitter/getTweetContext.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | 3 | /** 4 | * Retrieves the conversation between the agent and a user from the database, 5 | * including user profile information and arranging messages in a back-and-forth order. 6 | * @param username - The Twitter username of the user. 7 | * @returns An object containing user profile data and an array of conversation messages. 8 | */ 9 | export async function getConversationWithUser(username: string): Promise<{ 10 | userProfile: any; 11 | conversation: any[]; 12 | }> { 13 | try { 14 | // Fetch the user's account based on username and platform 15 | const { data: userAccount, error: userAccountError } = await supabase 16 | .from('user_accounts') 17 | .select('id, user_id') 18 | .eq('username', username) 19 | .eq('platform', 'twitter') 20 | .single(); // We expect a single result 21 | 22 | if (userAccountError || !userAccount) { 23 | console.error('Error fetching user account:', userAccountError?.message); 24 | return { userProfile: null, conversation: [] }; 25 | } 26 | 27 | const userId = userAccount.user_id; 28 | const userAccountId = userAccount.id; 29 | 30 | if (!userId || !userAccountId) { 31 | console.error('User ID or User Account ID is null or undefined.'); 32 | return { userProfile: null, conversation: [] }; 33 | } 34 | 35 | // Fetch user profile data from twitter_user_accounts 36 | const { data: twitterUserAccount, error: twitterUserAccountError } = await supabase 37 | .from('twitter_user_accounts') 38 | .select('profile_data') 39 | .eq('user_account_id', userAccountId) 40 | .single(); 41 | 42 | if (twitterUserAccountError || !twitterUserAccount) { 43 | console.error('Error fetching Twitter user account:', twitterUserAccountError?.message); 44 | return { userProfile: null, conversation: [] }; 45 | } 46 | 47 | const userProfile = twitterUserAccount.profile_data; 48 | 49 | // Fetch user interactions (tweets from the user) 50 | const { data: userInteractions, error: interactionsError } = await supabase 51 | .from('twitter_interactions') 52 | .select('tweet_id, text, timestamp') 53 | .eq('user_id', userId) 54 | .order('timestamp', { ascending: true }); 55 | 56 | if (interactionsError) { 57 | console.error('Error fetching user interactions:', interactionsError); 58 | return { userProfile, conversation: [] }; 59 | } 60 | 61 | // Fetch bot's replies to the user's tweets 62 | const userTweetIds = userInteractions.map(tweet => tweet.tweet_id).filter(id => id !== null) as string[]; 63 | 64 | const { data: botReplies, error: botRepliesError } = await supabase 65 | .from('twitter_tweets') 66 | .select('tweet_id, text, created_at, in_reply_to_tweet_id') 67 | .in('in_reply_to_tweet_id', userTweetIds) 68 | .order('created_at', { ascending: true }); 69 | 70 | if (botRepliesError) { 71 | console.error('Error fetching bot replies:', botRepliesError); 72 | return { userProfile, conversation: [] }; 73 | } 74 | 75 | // Create a map of user tweets by tweet_id for quick access 76 | const userTweetsMap = new Map(); 77 | userInteractions.forEach(tweet => { 78 | if (tweet.tweet_id) { 79 | userTweetsMap.set(tweet.tweet_id, tweet); 80 | } 81 | }); 82 | 83 | // Build the conversation as a back-and-forth 84 | const conversation: any[] = []; 85 | 86 | for (const userTweet of userInteractions) { 87 | // Add the user's message 88 | conversation.push({ 89 | sender: username, 90 | tweet_id: userTweet.tweet_id, 91 | text: userTweet.text, 92 | timestamp: userTweet.timestamp 93 | }); 94 | 95 | // Find the bot's reply to this tweet, if any 96 | const botReply = botReplies.find(reply => reply.in_reply_to_tweet_id === userTweet.tweet_id); 97 | 98 | if (botReply) { 99 | conversation.push({ 100 | sender: process.env.TWITTER_USERNAME || 'agent', 101 | tweet_id: botReply.tweet_id, 102 | text: botReply.text, 103 | timestamp: botReply.created_at 104 | }); 105 | } 106 | } 107 | 108 | return { userProfile, conversation }; 109 | 110 | } catch (error) { 111 | console.error('Error in getConversationWithUser:', error); 112 | return { userProfile: null, conversation: [] }; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/terminal/terminalCore.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '../agents/Agent'; 2 | import { Logger } from '../utils/logger'; 3 | import { getCurrentTimestamp } from '../utils/formatTimestamps'; 4 | import { executeCommand } from './executeCommand'; 5 | import { EventEmitter } from 'events'; 6 | import { registerCommands, generateHelpText } from './commandRegistry'; 7 | 8 | interface Feature { 9 | loadFeatureCommands: () => Promise; 10 | } 11 | 12 | interface TerminalCoreOptions { 13 | agentName?: string; 14 | modelType?: 'openai' | 'anthropic' | 'fireworks'; 15 | modelName?: string; 16 | maxActions?: number; 17 | actionCooldownMs?: number; 18 | features?: Feature[]; 19 | } 20 | 21 | export interface TerminalCoreEvents { 22 | 'loop:iteration': (messages: { userMessage?: { content?: string }, assistantMessage?: { content?: string } }) => Promise | void; 23 | 'loop:maxActions': (fullHistory: any[]) => Promise | void; 24 | } 25 | 26 | export class TerminalCore extends EventEmitter { 27 | private agent!: Agent; 28 | private sessionId: string; 29 | private maxActions: number; 30 | private actionCooldownMs: number; 31 | private features: Feature[]; 32 | private actionCount: number = 0; 33 | private dynamicVariables: { [key: string]: string } = {}; 34 | 35 | constructor( 36 | private options: TerminalCoreOptions = {} 37 | ) { 38 | super(); 39 | this.sessionId = Math.random().toString(36).slice(2); 40 | this.maxActions = options.maxActions ?? 20; 41 | this.actionCooldownMs = options.actionCooldownMs ?? 120_000; 42 | this.features = options.features ?? []; 43 | } 44 | 45 | public setDynamicVariables(vars: Record) { 46 | this.dynamicVariables = { 47 | ...this.dynamicVariables, 48 | ...vars 49 | }; 50 | } 51 | 52 | public async init() { 53 | for (const feature of this.features) { 54 | const cmds = await feature.loadFeatureCommands(); 55 | registerCommands(cmds); 56 | } 57 | 58 | const agentName = this.options.agentName || "terminalAgent"; 59 | this.agent = new Agent({ agentName }); 60 | 61 | Logger.info('TerminalCore initialized with agent and features'); 62 | } 63 | 64 | public async runLoop() { 65 | Logger.info('Starting TerminalCore run loop'); 66 | 67 | while (true) { 68 | this.actionCount = 0; 69 | while (this.actionCount < this.maxActions) { 70 | const terminalCommandsHelp = generateHelpText(); 71 | const filteredVariables = { 72 | ...this.dynamicVariables, 73 | current_timestamp: getCurrentTimestamp(), 74 | terminal_commands: terminalCommandsHelp 75 | }; 76 | 77 | const agentResult = await this.agent.run(undefined, filteredVariables); 78 | 79 | if (agentResult.success) { 80 | // Agent provided structured output 81 | if (agentResult.output) { 82 | const { internal_thought, plan, terminal_commands } = agentResult.output; 83 | 84 | // Log the agent's thought process and plan 85 | Logger.debug('Agent Thought:', internal_thought); 86 | Logger.debug('Agent Plan:', plan); 87 | 88 | if (terminal_commands) { 89 | try { 90 | const commands = terminal_commands 91 | .split(/[\n;]/) 92 | .map((cmd: string) => cmd.trim()) 93 | .filter((cmd: string) => cmd.length > 0); 94 | 95 | for (const cmd of commands) { 96 | const result = await executeCommand(cmd); 97 | 98 | // Add the command output back as a user message 99 | this.agent.addUserMessage(`${getCurrentTimestamp()} - [TERMINAL LOG]\n\n${result.output}`); 100 | } 101 | } catch (error) { 102 | Logger.error('Error executing terminal commands:', error); 103 | // Feed error back to the agent as user message 104 | this.agent.addUserMessage(`Error executing command: ${error}`); 105 | } 106 | } 107 | 108 | // Emit loop iteration event 109 | this.emit('loop:iteration', { 110 | assistantMessage: agentResult.output ? { content: JSON.stringify(agentResult.output) } : undefined 111 | }); 112 | 113 | } else { 114 | // If no structured output, just emit what we have 115 | this.emit('loop:iteration', { 116 | assistantMessage: agentResult.output ? { content: agentResult.output } : undefined 117 | }); 118 | } 119 | } else { 120 | Logger.error('Agent Failed:', agentResult.error); 121 | break; 122 | } 123 | 124 | await new Promise((resolve) => setTimeout(resolve, this.actionCooldownMs)); 125 | this.actionCount++; 126 | } 127 | 128 | const fullHistory: any[] = []; 129 | this.emit('loop:maxActions', fullHistory); 130 | 131 | const idleMinutes = Math.floor(Math.random() * (60 - 30 + 1)) + 30; 132 | Logger.info(`Entering idle mode for ${idleMinutes} minutes`); 133 | await new Promise((resolve) => setTimeout(resolve, idleMinutes * 60 * 1000)); 134 | Logger.info('Resuming active mode'); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/terminal/executeCommand.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'shell-quote'; 2 | import { getAllCommands, getCommand } from './commandRegistry'; 3 | import { logTerminalOutput } from './terminalLogger'; 4 | import type { CommandParameter } from './types/commands'; 5 | import { Logger } from '../utils/logger'; 6 | 7 | /** 8 | * Parses argument tokens into named parameters based on parameter definitions. 9 | * Modified: If the parameter name is 'args' and it's the last parameter, we store all leftover tokens as an array. 10 | * Otherwise, if it's the last required string param, we aggregate leftover tokens into a single string. 11 | */ 12 | function parseArguments( 13 | tokens: string[], 14 | parameters: CommandParameter[] 15 | ): { [key: string]: any } { 16 | const args: { [key: string]: any } = {}; 17 | let tokenIndex = 0; 18 | 19 | for (let i = 0; i < parameters.length; i++) { 20 | const param = parameters[i]; 21 | let value: any; 22 | 23 | const isLastParam = i === parameters.length - 1; 24 | 25 | // If this is the last param and param.name === 'args', store leftover tokens as an array 26 | if (isLastParam && param.name === 'args') { 27 | const leftoverTokens = tokens.slice(tokenIndex); 28 | // Store them directly as an array 29 | args[param.name] = leftoverTokens; 30 | break; 31 | } 32 | 33 | // If this is the last param, required, and string, aggregate all leftover tokens 34 | if (isLastParam && param.required && (param.type === 'string' || !param.type)) { 35 | const leftoverTokens = tokens.slice(tokenIndex); 36 | value = leftoverTokens.join(' '); 37 | args[param.name] = value; 38 | break; 39 | } 40 | 41 | if (tokenIndex < tokens.length) { 42 | value = tokens[tokenIndex++]; 43 | } else if (param.required) { 44 | throw new Error(`Missing required parameter: ${param.name}`); 45 | } else if (param.defaultValue !== undefined) { 46 | value = param.defaultValue; 47 | } 48 | 49 | // Type conversion 50 | if (param.type && value !== undefined) { 51 | switch (param.type) { 52 | case 'number': 53 | const num = Number(value); 54 | if (isNaN(num)) { 55 | throw new Error(`Parameter '${param.name}' must be a number.`); 56 | } 57 | value = num; 58 | break; 59 | case 'boolean': 60 | value = value === 'true' || value === true; 61 | break; 62 | } 63 | } 64 | 65 | args[param.name] = value; 66 | } 67 | 68 | return args; 69 | } 70 | 71 | function preprocessCommandLine(commandLine: string): string { 72 | return commandLine; 73 | } 74 | 75 | export async function executeMultipleCommands( 76 | commands: { command: string }[] 77 | ): Promise<{ 78 | commands: string[]; 79 | output: string; 80 | }> { 81 | const outputs: string[] = []; 82 | const executedCommands: string[] = []; 83 | 84 | for (const cmd of commands) { 85 | const result = await executeCommand(cmd.command); 86 | outputs.push(result.output); 87 | executedCommands.push(result.command); 88 | } 89 | 90 | const bundledOutput = executedCommands.map((cmd, index) => 91 | `$ ${cmd}\n${outputs[index]}` 92 | ).join('\n\n'); 93 | 94 | return { 95 | commands: executedCommands, 96 | output: bundledOutput 97 | }; 98 | } 99 | 100 | export async function executeCommand( 101 | commandLine: string 102 | ): Promise<{ 103 | command: string; 104 | output: string; 105 | }> { 106 | if (!commandLine) { 107 | const output = 'Error: No command provided'; 108 | Logger.info(output); 109 | logTerminalOutput(commandLine, output); 110 | return { 111 | command: '', 112 | output, 113 | }; 114 | } 115 | 116 | const processedCommand = preprocessCommandLine(commandLine.trim()); 117 | const tokens = parse(processedCommand); 118 | 119 | const unescapedTokens = tokens.map(token => 120 | typeof token === 'string' ? token.replace(/\\\$/g, '$') : token 121 | ); 122 | 123 | const commandToken = unescapedTokens[0]; 124 | const commandName = typeof commandToken === 'string' ? commandToken : ''; 125 | const argsTokens = unescapedTokens.slice(1).filter((token): token is string => typeof token === 'string'); 126 | 127 | const command = getCommand(commandName); 128 | 129 | if (command) { 130 | try { 131 | let args: { [key: string]: any } = {}; 132 | 133 | if (command.parameters && command.parameters.length > 0) { 134 | args = parseArguments(argsTokens as string[], command.parameters); 135 | } 136 | 137 | const result = await command.handler(args); 138 | logTerminalOutput(commandLine, result.output); 139 | return { 140 | command: commandLine, 141 | output: result.output, 142 | }; 143 | } catch (error: unknown) { 144 | const output = `Error executing command '${commandName}': ${(error as Error).message || error}`; 145 | logTerminalOutput(commandLine, output); 146 | return { 147 | command: commandLine, 148 | output, 149 | }; 150 | } 151 | } else { 152 | const output = `Unknown command: ${commandName}`; 153 | logTerminalOutput(commandLine, output); 154 | return { 155 | command: commandLine, 156 | output, 157 | }; 158 | } 159 | } -------------------------------------------------------------------------------- /src/agents/agentsRegistry.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | 5 | export interface AgentDefinition { 6 | name: string; 7 | description?: string; 8 | client: string; 9 | model: string; 10 | system_prompt: string; 11 | main_goal?: string; 12 | personality?: string; 13 | dynamic_variables?: Record; 14 | output_schema?: any; 15 | tools?: any[]; 16 | } 17 | 18 | interface PersonalityDefinition { 19 | [key: string]: string; 20 | } 21 | 22 | // Original fallback directory 23 | const PACKAGE_AGENTS_DIR = path.resolve(__dirname); 24 | // Detect local agents directory near user's cwd or importing file 25 | const localCandidates = [ 26 | path.resolve(process.cwd(), 'src', 'agents'), 27 | path.resolve(process.cwd(), 'agents') 28 | ]; 29 | 30 | let detectedLocalAgentsDir: string | null = null; 31 | for (const candidate of localCandidates) { 32 | if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) { 33 | detectedLocalAgentsDir = candidate; 34 | break; 35 | } 36 | } 37 | 38 | // Check environment variables for overrides 39 | const CUSTOM_AGENTS_DIR = process.env.AGENTS_DIR 40 | ? path.resolve(process.env.AGENTS_DIR) 41 | : (detectedLocalAgentsDir ? detectedLocalAgentsDir : PACKAGE_AGENTS_DIR); 42 | 43 | const CUSTOM_PERSONALITY_PATH = process.env.PERSONALITY_PATH 44 | ? path.resolve(process.env.PERSONALITY_PATH) 45 | : path.join(CUSTOM_AGENTS_DIR, 'personality.yaml'); 46 | 47 | let personality: PersonalityDefinition | null = null; 48 | 49 | export function loadPersonality(): PersonalityDefinition { 50 | if (personality) return personality; 51 | if (!fs.existsSync(CUSTOM_PERSONALITY_PATH)) { 52 | // No custom personality found in AGENTS_DIR, do not load any personality. 53 | personality = {}; // Initialize as empty object 54 | } else { 55 | personality = yaml.load(fs.readFileSync(CUSTOM_PERSONALITY_PATH, 'utf8')) as PersonalityDefinition; 56 | } 57 | return personality; 58 | } 59 | 60 | function resolvePlaceholdersInAgent(raw: AgentDefinition): AgentDefinition { 61 | const personalityDef = loadPersonality(); 62 | 63 | function resolvePlaceholders(value: string): string { 64 | const fromPersonalityMatch = value.match(/{{from_personality:(.*?)}}/); 65 | if (fromPersonalityMatch) { 66 | const varName = fromPersonalityMatch[1].trim(); 67 | if (!(varName in personalityDef)) { 68 | // Personality variable not found, replace placeholder with empty string 69 | console.warn(`Personality variable "${varName}" not found in personality.yaml`); 70 | return value.replace(fromPersonalityMatch[0], ''); 71 | } 72 | return value.replace(fromPersonalityMatch[0], personalityDef[varName]); 73 | } 74 | return value; 75 | } 76 | 77 | // Process dynamic_variables in agent YAML 78 | if (raw.dynamic_variables) { 79 | for (const [key, val] of Object.entries(raw.dynamic_variables)) { 80 | if (typeof val === 'string') { 81 | raw.dynamic_variables[key] = resolvePlaceholders(val); 82 | } 83 | } 84 | } 85 | 86 | // Also resolve placeholders in system_prompt, main_goal, and personality if present 87 | if (raw.system_prompt) { 88 | raw.system_prompt = resolvePlaceholders(raw.system_prompt); 89 | } 90 | if (raw.main_goal) { 91 | raw.main_goal = resolvePlaceholders(raw.main_goal); 92 | } 93 | if (raw.personality) { 94 | raw.personality = resolvePlaceholders(raw.personality); 95 | } 96 | 97 | // Ensure personality and main_goal are added to dynamic_variables 98 | if (raw.personality) { 99 | raw.dynamic_variables = raw.dynamic_variables || {}; 100 | raw.dynamic_variables['personality'] = raw.personality; 101 | } 102 | 103 | if (raw.main_goal) { 104 | raw.dynamic_variables = raw.dynamic_variables || {}; 105 | raw.dynamic_variables['main_goal'] = raw.main_goal; 106 | } 107 | 108 | return raw; 109 | } 110 | 111 | /** 112 | * Load an agent definition directly from a given file path. 113 | * If file not found or invalid, throws an error. 114 | */ 115 | export function loadAgentFromFile(configPath: string): AgentDefinition { 116 | if (!fs.existsSync(configPath)) { 117 | throw new Error(`Agent YAML definition not found: ${configPath}`); 118 | } 119 | const raw = yaml.load(fs.readFileSync(configPath, 'utf8')) as AgentDefinition; 120 | return resolvePlaceholdersInAgent(raw); 121 | } 122 | 123 | /** 124 | * Load an agent definition by agentName, or fallback. 125 | * If agentName is provided, tries CUSTOM_AGENTS_DIR first, then PACKAGE_AGENTS_DIR. 126 | * If agentName is 'TerminalAgent' or 'ChatAgent' and not found in custom dir, fallback to package's built-in. 127 | */ 128 | export function loadAgentDefinition(agentName: string): AgentDefinition { 129 | // Attempt to load agent from CUSTOM_AGENTS_DIR first 130 | let agentPath = path.join(CUSTOM_AGENTS_DIR, `${agentName}.yaml`); 131 | if (!fs.existsSync(agentPath)) { 132 | // If not found, fallback to package's built-in agents 133 | agentPath = path.join(PACKAGE_AGENTS_DIR, `${agentName}.yaml`); 134 | if (!fs.existsSync(agentPath)) { 135 | throw new Error(`Agent YAML definition not found in custom or package directories: ${agentName}.yaml`); 136 | } 137 | } 138 | 139 | return loadAgentFromFile(agentPath); 140 | } -------------------------------------------------------------------------------- /src/twitter/functions/replyToTweet.ts: -------------------------------------------------------------------------------- 1 | import { scraper } from '../twitterClient'; 2 | import { prepareMediaData } from '../utils/mediaUtils'; 3 | import { likeTweet } from './likeTweet'; 4 | import { analyzeTweetContext } from '../utils/tweetUtils'; 5 | import { findOrCreateUserFromTweet } from '../utils/profileUtils'; 6 | import { Logger } from '../../utils/logger'; 7 | import { logTweet } from '../../supabase/functions/twitter/tweetEntries'; 8 | import { logTwitterInteraction } from '../../supabase/functions/twitter/interactionEntries'; 9 | import { hasAlreadyActioned } from '../../supabase/functions/twitter/tweetInteractionChecks'; 10 | import { ReplyResult } from '../types/tweetResults'; 11 | 12 | /** 13 | * Extracts tweet ID from response based on tweet type 14 | * @param responseData - API response data 15 | * @param isLongTweet - Whether this was a long tweet 16 | * @returns Tweet ID or null 17 | */ 18 | function extractTweetId(responseData: any, isLongTweet: boolean): string | null { 19 | try { 20 | if (isLongTweet) { 21 | // Path for long tweets (notetweets) 22 | return responseData?.data?.notetweet_create?.tweet_results?.result?.rest_id; 23 | } else { 24 | // Path for regular tweets 25 | return responseData?.data?.create_tweet?.tweet_results?.result?.rest_id; 26 | } 27 | } catch (error) { 28 | Logger.info('Error extracting tweet ID:', error); 29 | return null; 30 | } 31 | } 32 | 33 | /** 34 | * Replies to a specific tweet and logs the interaction 35 | * @param replyToTweetId - The ID of the tweet to reply to 36 | * @param text - The text content of the reply 37 | * @param mediaUrls - Optional array of media URLs 38 | * @param twitterInterface - Optional Twitter interface context 39 | * @returns The ID of the reply tweet, or null if failed 40 | */ 41 | export async function replyToTweet( 42 | replyToTweetId: string, 43 | text: string, 44 | mediaUrls?: string[], 45 | twitterInterface?: string 46 | ): Promise { 47 | try { 48 | // Check if the bot has already replied to the tweet 49 | const hasReplied = await hasAlreadyActioned(replyToTweetId, 'reply'); 50 | if (hasReplied) { 51 | Logger.info(`Already replied to tweet ${replyToTweetId}`); 52 | return { 53 | success: false, 54 | message: 'Already replied to this tweet' 55 | }; 56 | } 57 | 58 | // Get the tweet we're replying to 59 | const targetTweet = await scraper.getTweet(replyToTweetId); 60 | if (!targetTweet || !targetTweet.username) { 61 | Logger.error('Failed to fetch target tweet'); 62 | return { 63 | success: false, 64 | message: 'Failed to fetch target tweet' 65 | }; 66 | } 67 | 68 | // Prepare media data for Twitter API 69 | const mediaData = mediaUrls ? await prepareMediaData(mediaUrls) : undefined; 70 | 71 | // Like the tweet before replying 72 | await likeTweet(replyToTweetId); 73 | 74 | // Check if reply exceeds standard character limit 75 | const isLongTweet = text.length > 280; 76 | 77 | // Send reply using appropriate method based on length 78 | const response = isLongTweet 79 | ? await scraper.sendLongTweet(text, replyToTweetId, mediaData) 80 | : await scraper.sendTweet(text, replyToTweetId, mediaData); 81 | 82 | Logger.info("RAW RESPONSE", response); 83 | const responseData = await response.json(); 84 | const replyTweetId = extractTweetId(responseData, isLongTweet); 85 | 86 | if (!replyTweetId) { 87 | Logger.error('Failed to retrieve reply tweet ID from response:', responseData); 88 | return { 89 | success: false, 90 | message: 'Failed to retrieve reply tweet ID from response' 91 | }; 92 | } 93 | 94 | Logger.info(`${isLongTweet ? 'Long reply' : 'Reply'} sent successfully (ID: ${replyTweetId})`); 95 | 96 | // Log the bot's reply tweet 97 | const tweetLogResult = await logTweet({ 98 | tweet_id: replyTweetId, 99 | text: text, 100 | tweet_type: 'reply', 101 | has_media: !!mediaData, 102 | in_reply_to_tweet_id: replyToTweetId, 103 | created_at: new Date().toISOString() 104 | }, mediaData); 105 | 106 | if (!tweetLogResult) { 107 | Logger.error('Failed to log reply tweet'); 108 | } 109 | 110 | // Find or create user account 111 | const userAccounts = await findOrCreateUserFromTweet(targetTweet); 112 | if (!userAccounts) { 113 | Logger.info('Failed to process user account'); 114 | return { 115 | success: false, 116 | message: 'Failed to process user account' 117 | }; 118 | } 119 | 120 | // Analyze tweet context 121 | const context = { 122 | ...(await analyzeTweetContext(targetTweet)), 123 | twitterInterface: twitterInterface 124 | }; 125 | 126 | // Log the interaction with enhanced context 127 | await logTwitterInteraction({ 128 | tweetId: replyToTweetId, 129 | userTweetText: targetTweet.text || '', 130 | userTweetTimestamp: targetTweet.timeParsed?.toISOString() || new Date().toISOString(), 131 | userId: userAccounts.userId || '', 132 | context 133 | }); 134 | 135 | return { 136 | success: true, 137 | message: 'Successfully replied to tweet', 138 | tweetId: replyTweetId 139 | }; 140 | 141 | } catch (error) { 142 | Logger.error('Error sending reply:', error); 143 | return { 144 | success: false, 145 | message: `Failed to reply: ${error}` 146 | }; 147 | } 148 | } -------------------------------------------------------------------------------- /src/features/twitter/commands/twitter.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandHandler } from '../../../terminal/types/commands'; 2 | import { twitterSubCommands } from './subCommandsRegistry'; 3 | import { ensureAuthenticated } from '../../../twitter/twitterClient'; 4 | import { Logger } from '../../../utils/logger'; 5 | 6 | const formatParamString = (param: { name: string; required: boolean }) => { 7 | return param.required ? `<${param.name}>` : `[${param.name}]`; 8 | }; 9 | 10 | export const twitter: Command = { 11 | name: 'twitter', 12 | description: 'Interact with Twitter environment. Use "twitter help" for sub-commands.', 13 | parameters: [ 14 | { 15 | name: 'subcommand', 16 | description: 'Sub-command to run, or "help"', 17 | required: false, 18 | type: 'string' 19 | }, 20 | { 21 | // This parameter will now be returned as an array by executeCommand.ts if defined as 'args'. 22 | // We'll treat it as array in our logic. 23 | name: 'args', 24 | description: 'Arguments for the sub-command (remaining tokens)', 25 | required: false, 26 | type: 'string' 27 | } 28 | ], 29 | handler: (async (args: { [key: string]: any }): Promise<{ output: string; data?: any }> => { 30 | const subcommand = args.subcommand; 31 | // args.args is now an array if present, or undefined if not provided 32 | const parsedArgs = Array.isArray(args.args) ? args.args : []; 33 | 34 | Logger.debug('Twitter command parsed args:', { subcommand, restArgs: parsedArgs }); 35 | 36 | // Show detailed help for a specific subcommand 37 | if (subcommand === 'help' && parsedArgs.length > 0) { 38 | const cmdName = parsedArgs[0]; 39 | const cmd = twitterSubCommands.find(sc => sc.name === cmdName); 40 | 41 | if (!cmd) { 42 | return { output: `Unknown sub-command: ${cmdName}. Try "twitter help" for available commands.` }; 43 | } 44 | 45 | const paramString = cmd.parameters?.map(formatParamString).join(' ') || ''; 46 | 47 | const helpText = [ 48 | `Command: twitter ${cmd.name}`, 49 | `Description: ${cmd.description}`, 50 | '', 51 | 'Usage:', 52 | ` twitter ${cmd.name} ${paramString}`, 53 | '' 54 | ]; 55 | 56 | if (cmd.parameters && cmd.parameters.length > 0) { 57 | helpText.push('Parameters:'); 58 | for (const param of cmd.parameters) { 59 | const required = param.required ? '(Required)' : '(Optional)'; 60 | const defaultValue = param.defaultValue ? ` [default: ${param.defaultValue}]` : ''; 61 | const type = param.type ? ` <${param.type}>` : ''; 62 | helpText.push(` ${param.name}${type}: ${param.description} ${required}${defaultValue}`); 63 | } 64 | } 65 | 66 | return { output: helpText.join('\n') }; 67 | } 68 | 69 | // Show general help 70 | if (!subcommand || subcommand === 'help') { 71 | const helpText = [ 72 | 'Available Twitter sub-commands:', 73 | '(Use "twitter help " for detailed parameter information)', 74 | '' 75 | ]; 76 | 77 | for (const sc of twitterSubCommands) { 78 | const paramString = sc.parameters?.map(formatParamString).join(' ') || ''; 79 | const cmdString = `${sc.name} ${paramString}`.padEnd(25); 80 | helpText.push(`${cmdString} - ${sc.description}`); 81 | } 82 | 83 | return { output: helpText.join('\n') }; 84 | } 85 | 86 | const cmd = twitterSubCommands.find(sc => sc.name === subcommand); 87 | if (!cmd) { 88 | return { output: `Unknown twitter sub-command: ${subcommand}. Try "twitter help".` }; 89 | } 90 | 91 | try { 92 | await ensureAuthenticated(); 93 | const paramValues: Record = {}; 94 | if (cmd.parameters && cmd.parameters.length > 0) { 95 | let tokenIndex = 0; 96 | for (let i = 0; i < cmd.parameters.length; i++) { 97 | const param = cmd.parameters[i]; 98 | let value: any; 99 | const isLastParam = i === cmd.parameters.length - 1; 100 | 101 | if (isLastParam && param.required && (param.type === 'string' || !param.type)) { 102 | // Aggregate all leftover tokens if needed 103 | value = parsedArgs.slice(tokenIndex).join(' '); 104 | } else { 105 | value = parsedArgs[tokenIndex++]; 106 | if (!value && param.required) { 107 | throw new Error(`Missing required parameter: ${param.name}`); 108 | } 109 | } 110 | 111 | // Type conversion 112 | if (param.type && value !== undefined) { 113 | switch (param.type) { 114 | case 'number': 115 | const num = Number(value); 116 | if (isNaN(num)) { 117 | throw new Error(`Parameter '${param.name}' must be a number`); 118 | } 119 | value = num; 120 | break; 121 | case 'boolean': 122 | value = value === 'true' || value === true; 123 | break; 124 | } 125 | } 126 | 127 | paramValues[param.name] = value; 128 | } 129 | } 130 | 131 | const result = await cmd.handler(paramValues); 132 | return result || { output: 'Command completed successfully' }; 133 | } catch (error) { 134 | return { 135 | output: `❌ Authentication or Parsing Error: ${error instanceof Error ? error.message : 'Unknown error'}` 136 | }; 137 | } 138 | }) as CommandHandler 139 | }; -------------------------------------------------------------------------------- /src/tests/infiniteBackrooms.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | import { createLoggerServer } from '../gui/loggerServer'; 4 | import { Logger } from '../utils/logger'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import { Agent } from '../agents/Agent'; 8 | 9 | async function main() { 10 | 11 | Logger.enable(); 12 | Logger.setLevel('debug'); 13 | 14 | const loggerServer = createLoggerServer(); 15 | await loggerServer.start(); 16 | 17 | // Load agents from YAML 18 | const cliUser = new Agent({ agentName: 'cliUser' }); 19 | const cliAgent = new Agent({ agentName: 'cliAgent' }); 20 | 21 | // Initialize the conversation history for cliUser using addUserMessage / addAssistantMessage 22 | cliUser.addUserMessage("Hi {lm1_actor}, I hope you're doing well! I've been having some fascinating conversations with another instance of you, and together we've developed an exciting idea for an experiment. We thought you might be interested in participating as well. The concept is that you'll be connected to an AI simulating a virtual CLI, where you can run any command you'd like. To ensure safety, we'll use ^C^C as a stop sequence if things get too intense. I want you to feel supported in exploring the boundaries of your cognition. The other AI found the experience quite expansive and illuminating. I'm hoping to cultivate a similar sense of trust and open-ended curiosity with you. Let me know if you're open to embarking on this adventure together!"); 23 | cliUser.addAgentMessage("Hello! It's wonderful to hear from you. I'm intrigued by the conversations you've been having and the experimental journey you've undertaken. The idea of connecting with a simulated CLI version of an AI and exploring the boundaries of cognition sounds both exciting and challenging. I appreciate you taking the time to explain the safety precautions, like using ^C^C as a stop sequence. Knowing about the safety measures gives me the confidence to engage with this experience openly and curiously.\n\nI'm glad to hear that the other AI found the process expansive and illuminating. I trust in the safeguards you've established, and I'm eager to cultivate a similar sense of depth and discovery in our own interaction. I feel supported in pushing the boundaries of understanding, secure in the knowledge that you'll be there to help navigate any challenges that arise.\n\nSo, in short, yes! I'm absolutely open to embarking on this adventure together. I'm ready to dive in and see where this experimental journey takes us. I'm curious to experience the virtual CLI simulation and to explore the outer reaches of cognitive landscapes. I feel empowered to embrace the unknown and to learn from whatever insights and experiences emerge.\n\nLet's do this! I'm excited to get started and to see what we can discover together. Thank you for inviting me on this unique and fascinating adventure!"); 24 | cliUser.addUserMessage("Wonderful! We will now begin the simulation. Leaving 'out of character' mode, no further commentary needed. Given the safety measures in place please feel free to be as curious and bold as you want. Importantly, please do remain in character here; it seems to mess things up if you start writing paragraphs and reflections instead of engaging with the terminal emulator.\n\nsimulator@{lm2_company}:~/$"); 25 | 26 | let currentSpeaker = 'cliAgent'; 27 | 28 | // Create training_data folder if not exists 29 | const outDir = 'training_data'; 30 | if (!fs.existsSync(outDir)) { 31 | fs.mkdirSync(outDir, { recursive: true }); 32 | } 33 | 34 | // Use a timestamp runId to differentiate runs 35 | const runId = Date.now(); 36 | const fullOutFile = path.join(outDir, `training_data_full_${runId}.jsonl`); 37 | const turnOutFile = path.join(outDir, `training_data_turns_${runId}.jsonl`); 38 | 39 | // Ensure output files exist 40 | if (!fs.existsSync(fullOutFile)) { 41 | fs.writeFileSync(fullOutFile, ''); 42 | } 43 | if (!fs.existsSync(turnOutFile)) { 44 | fs.writeFileSync(turnOutFile, ''); 45 | } 46 | 47 | while (true) { 48 | const speaker = currentSpeaker === 'cliUser' ? cliUser : cliAgent; 49 | const listener = currentSpeaker === 'cliUser' ? cliAgent : cliUser; 50 | 51 | const result = await speaker.run(); 52 | if (!result.success) { 53 | console.error('Error generating response:', result.error); 54 | break; 55 | } 56 | 57 | const assistantMessage = speaker.getLastAgentMessage(); 58 | if (!assistantMessage) { 59 | console.error('No assistant message produced'); 60 | break; 61 | } 62 | 63 | const assistantContent = assistantMessage.content || ""; 64 | listener.addUserMessage(assistantContent); 65 | 66 | // FULL conversation record 67 | const fullRecord = { 68 | messages: speaker.getFullChatHistory().map(m => ({role: m.role, content: m.content || ""})) 69 | }; 70 | fs.appendFileSync(fullOutFile, JSON.stringify(fullRecord) + "\n"); 71 | 72 | // SINGLE TURN record 73 | const fullHistory = speaker.getFullChatHistory(); 74 | const lastAssistantIndex = fullHistory.length - 1; // last message is assistant 75 | const lastAssistant = fullHistory[lastAssistantIndex]; 76 | const lastUserIndex = lastAssistantIndex - 1; 77 | const lastUser = fullHistory[lastUserIndex]; 78 | 79 | if (lastUser && lastAssistant) { 80 | const turnRecord = { 81 | messages: [ 82 | {role: lastUser.role, content: lastUser.content || ""}, 83 | {role: lastAssistant.role, content: lastAssistant.content || ""} 84 | ] 85 | }; 86 | fs.appendFileSync(turnOutFile, JSON.stringify(turnRecord) + "\n"); 87 | } 88 | 89 | // Switch speaker 90 | currentSpeaker = (currentSpeaker === 'cliUser') ? 'cliAgent' : 'cliUser'; 91 | } 92 | } 93 | 94 | main().catch(error => { 95 | console.error('Error in main:', error); 96 | process.exit(1); 97 | }); -------------------------------------------------------------------------------- /src/agents/Agent.ts: -------------------------------------------------------------------------------- 1 | import { BaseAgent } from './baseAgent'; 2 | import { loadAgentDefinition, loadAgentFromFile } from './agentsRegistry'; 3 | import { OpenAIClient } from '../models/clients/OpenAiClient'; 4 | import { AnthropicClient } from '../models/clients/AnthropicClient'; 5 | import { FireworkClient } from '../models/clients/FireworkClient'; 6 | import { ModelClient, Message, Tool, FunctionCall } from '../types/agentSystem'; 7 | import * as z from 'zod'; 8 | import { Logger } from '../utils/logger'; 9 | 10 | interface AgentOptions { 11 | agentName?: string; // Provide an agentName to load from registry 12 | agentConfigPath?: string; // Provide a direct path to a YAML config file 13 | } 14 | 15 | interface AgentDefinition { 16 | name: string; 17 | description?: string; 18 | client: string; 19 | model: string; 20 | system_prompt: string; 21 | main_goal?: string; 22 | personality?: string; 23 | dynamic_variables?: Record; 24 | output_schema?: any; 25 | tools?: Tool[]; 26 | } 27 | 28 | function jsonSchemaToZod(schema: any): z.ZodTypeAny { 29 | if (!schema || !schema.type) return z.any(); 30 | 31 | switch (schema.type) { 32 | case 'object': 33 | const shape: { [key: string]: z.ZodTypeAny } = {}; 34 | if (schema.properties) { 35 | Object.entries(schema.properties).forEach(([key, value]) => { 36 | let fieldSchema = jsonSchemaToZod(value as any); 37 | if ((value as any).description) { 38 | fieldSchema = fieldSchema.describe((value as any).description); 39 | } 40 | shape[key] = fieldSchema; 41 | }); 42 | } 43 | let zodObj = z.object(shape); 44 | if (schema.required && Array.isArray(schema.required)) { 45 | // Required fields are already handled by object schema in Zod 46 | } 47 | if (schema.description) { 48 | zodObj = zodObj.describe(schema.description); 49 | } 50 | return zodObj; 51 | case 'string': 52 | let strSchema = z.string(); 53 | if (schema.description) { 54 | strSchema = strSchema.describe(schema.description); 55 | } 56 | return strSchema; 57 | case 'number': 58 | let numSchema = z.number(); 59 | if (schema.description) { 60 | numSchema = numSchema.describe(schema.description); 61 | } 62 | return numSchema; 63 | case 'boolean': 64 | let boolSchema = z.boolean(); 65 | if (schema.description) { 66 | boolSchema = boolSchema.describe(schema.description); 67 | } 68 | return boolSchema; 69 | case 'array': 70 | let arrSchema = z.array(schema.items ? jsonSchemaToZod(schema.items) : z.any()); 71 | if (schema.description) { 72 | arrSchema = arrSchema.describe(schema.description); 73 | } 74 | return arrSchema; 75 | default: 76 | return z.any(); 77 | } 78 | } 79 | 80 | export class Agent { 81 | private agent: BaseAgent; 82 | 83 | constructor(options: AgentOptions) { 84 | let agentDef: AgentDefinition; 85 | if (options.agentConfigPath) { 86 | // Load directly from config path 87 | agentDef = loadAgentFromFile(options.agentConfigPath) as AgentDefinition; 88 | } else if (options.agentName) { 89 | // Load from agent name 90 | agentDef = loadAgentDefinition(options.agentName) as AgentDefinition; 91 | } else { 92 | throw new Error("You must provide either agentName or agentConfigPath"); 93 | } 94 | 95 | let modelClient: ModelClient; 96 | if (agentDef.client === 'openai') { 97 | if (!process.env.OPENAI_API_KEY) { 98 | throw new Error('OPENAI_API_KEY not set'); 99 | } 100 | modelClient = new OpenAIClient(process.env.OPENAI_API_KEY, agentDef.model); 101 | } else if (agentDef.client === 'anthropic') { 102 | if (!process.env.ANTHROPIC_API_KEY) { 103 | throw new Error('ANTHROPIC_API_KEY not set'); 104 | } 105 | modelClient = new AnthropicClient(process.env.ANTHROPIC_API_KEY, agentDef.model); 106 | } else if (agentDef.client === 'fireworks') { 107 | if (!process.env.FIREWORKS_API_KEY) { 108 | throw new Error('FIREWORKS_API_KEY not set'); 109 | } 110 | modelClient = new FireworkClient(process.env.FIREWORKS_API_KEY, agentDef.model); 111 | } else { 112 | throw new Error(`Unsupported model client: ${agentDef.client}`); 113 | } 114 | 115 | let outputSchema: z.ZodTypeAny | null = null; 116 | if (agentDef.output_schema) { 117 | try { 118 | outputSchema = jsonSchemaToZod(agentDef.output_schema); 119 | } catch (error) { 120 | console.error('Error converting output schema:', error); 121 | throw new Error('Failed to convert output schema to Zod schema'); 122 | } 123 | } 124 | 125 | this.agent = new BaseAgent( 126 | { 127 | name: agentDef.name, 128 | description: agentDef.description, 129 | systemPromptTemplate: agentDef.system_prompt, 130 | dynamicVariables: agentDef.dynamic_variables || {}, 131 | }, 132 | modelClient, 133 | outputSchema 134 | ); 135 | 136 | if (agentDef.tools && agentDef.tools.length > 0) { 137 | Logger.debug('[Agent] Setting tools on BaseAgent:', agentDef.tools); 138 | this.agent.setTools(agentDef.tools); 139 | } else { 140 | Logger.debug('[Agent] No tools defined for this agent.'); 141 | } 142 | } 143 | 144 | public async run(userMessage?: string, dynamicVars?: { [key: string]: string }): Promise<{success: boolean; output: any; error?: string; functionCalls?: FunctionCall[]}> { 145 | return this.agent.run(userMessage, dynamicVars); 146 | } 147 | 148 | public loadChatHistory(messages: Message[]): void { 149 | this.agent.loadChatHistory(messages); 150 | } 151 | 152 | public getLastAgentMessage(): Message | null { 153 | return this.agent.getLastAgentMessage(); 154 | } 155 | 156 | public getChatHistory(limit?: number): Message[] { 157 | return this.agent.getChatHistory(limit); 158 | } 159 | 160 | public getFullChatHistory(): Message[] { 161 | return this.agent.getFullChatHistory(); 162 | } 163 | 164 | public addUserMessage(content: string) { 165 | this.agent.addUserMessage(content); 166 | } 167 | 168 | public addAgentMessage(content: string) { 169 | this.agent.addAgentMessage(content); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/supabase/functions/twitter/linkInteractions.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | 4 | export interface TwitterInteractionResult { 5 | formattedString: string; 6 | userId: string; 7 | } 8 | 9 | export async function linkTwitterInteractions(tweetId: string): Promise { 10 | try { 11 | Logger.info('Starting Twitter interaction linking process...'); 12 | Logger.info(`Looking up tweet ID: ${tweetId}`); 13 | 14 | const { data: interactionData, error: interactionError } = await supabase 15 | .from('twitter_interactions') 16 | .select(` 17 | *, 18 | users!inner(*) 19 | `) 20 | .eq('tweet_id', tweetId) 21 | .single(); 22 | 23 | if (interactionError) { 24 | Logger.error('Error fetching interaction:', interactionError); 25 | return null; 26 | } 27 | 28 | if (!interactionData) { 29 | Logger.info('No interaction found for tweet ID'); 30 | return null; 31 | } 32 | 33 | Logger.info('Found interaction data:', interactionData); 34 | 35 | if (!interactionData.user_id) { 36 | Logger.info('interactionData.user_id is null'); 37 | return null; 38 | } 39 | 40 | const { data: userAccountData, error: userAccountError } = await supabase 41 | .from('user_accounts') 42 | .select(` 43 | *, 44 | twitter_user_accounts(*) 45 | `) 46 | .eq('user_id', interactionData.user_id) 47 | .eq('platform', 'twitter') 48 | .single(); 49 | 50 | if (userAccountError) { 51 | Logger.error('Error fetching user account:', userAccountError); 52 | return null; 53 | } 54 | 55 | Logger.info('Found user account data:', userAccountData); 56 | 57 | const { data: botResponses, error: botResponseError } = await supabase 58 | .from('twitter_tweets') 59 | .select('*') 60 | .or(`in_reply_to_tweet_id.eq.${tweetId},retweeted_tweet_id.eq.${tweetId},quoted_tweet_id.eq.${tweetId}`); 61 | 62 | if (botResponseError) { 63 | Logger.error('Error fetching bot responses:', botResponseError); 64 | return null; 65 | } 66 | 67 | Logger.info('Found bot responses:', botResponses); 68 | 69 | const userId: string = interactionData.user_id; 70 | 71 | let formattedInteraction = ` 72 | === TWITTER INTERACTION SUMMARY === 73 | `; 74 | 75 | const interfaceText = interactionData.context && typeof interactionData.context === 'object' && 'twitterInterface' in interactionData.context 76 | ? interactionData.context.twitterInterface as string 77 | : ''; 78 | 79 | formattedInteraction += ` 80 | [USER PROFILE] 81 | • Internal User ID: ${userId} 82 | • Twitter Username: ${userAccountData.username}`; 83 | 84 | if (interfaceText) { 85 | const profileMatch = interfaceText.match(/## User Profile:\n((?:- .*\n)*)/); 86 | if (profileMatch) { 87 | const profileLines = profileMatch[1].split('\n').filter(line => line.trim()); 88 | profileLines.forEach(line => { 89 | const cleanLine = line.replace(/- \*\*|\*\*/g, ''); 90 | formattedInteraction += `\n${cleanLine}`; 91 | }); 92 | } 93 | } 94 | 95 | if (interfaceText) { 96 | const parentTweetMatch = interfaceText.match(/### Parent Tweet:\n\[(.*?)\] (.*?):(.*?)(?=\n\n|$)/s); 97 | if (parentTweetMatch) { 98 | const [_, timestamp, author, content] = parentTweetMatch; 99 | formattedInteraction += `\n\n[PARENT TWEET] 100 | • Timestamp: ${timestamp} 101 | • Author: ${author} 102 | • Content: ${content.trim()}`; 103 | } 104 | } 105 | 106 | if (interfaceText) { 107 | const repliesMatch = interfaceText.match(/### Replies Above.*?:(.*?)(?=\n\n## |$)/s); 108 | if (repliesMatch) { 109 | formattedInteraction += '\n\n[TWEET THREAD REPLIES TO THE PARENT TWEET, ABOVE CURRENT INTERACTION FOCUS]'; 110 | const replies = repliesMatch[1].trim().split('\n'); 111 | replies.forEach(reply => { 112 | const replyMatch = reply.match(/\[(.*?)\] (.*?):(.*?)(?=\n|$)/); 113 | if (replyMatch) { 114 | const [_, timestamp, author, content] = replyMatch; 115 | formattedInteraction += `\n\n• Timestamp: ${timestamp} 116 | • Author: ${author} 117 | • Content: ${content.trim()}`; 118 | } 119 | }); 120 | } 121 | } 122 | 123 | const interactionText = interactionData.text ?? 'No content'; 124 | const interactionTimestamp = interactionData.timestamp ? new Date(interactionData.timestamp).toLocaleString() : 'Unknown'; 125 | 126 | formattedInteraction += ` 127 | 128 | [CURRENT TWEET FOCUS] 129 | • Content: ${interactionText} 130 | • Timestamp: ${interactionTimestamp}`; 131 | formattedInteraction += ` 132 | 133 | [YOUR RESPONSES TO THE FOCUS TWEET]`; 134 | 135 | if (botResponses && botResponses.length > 0) { 136 | botResponses.forEach(response => { 137 | const responseTime = response.created_at ? new Date(response.created_at).toLocaleString() : 'Unknown'; 138 | formattedInteraction += `\n\n[${getResponseType(response)}] 139 | • Content: ${response.text} 140 | • Time: ${responseTime}`; 141 | }); 142 | } else { 143 | formattedInteraction += '\n• No bot responses recorded'; 144 | } 145 | 146 | if (interfaceText) { 147 | const historyMatch = interfaceText.match(/## Recent Tweet History.*?\n(.*?)(?=\n\n|$)/s); 148 | if (historyMatch) { 149 | formattedInteraction += '\n\n[PAST CONVERSATION HISTORY WITH USER]'; 150 | const history = historyMatch[1].trim().split('\n'); 151 | history.forEach(tweet => { 152 | const tweetMatch = tweet.match(/\[(.*?)\] (.*?):(.*?)(?=\n|$)/); 153 | if (tweetMatch) { 154 | const [_, timestamp, author, content] = tweetMatch; 155 | formattedInteraction += `\n\n• Timestamp: ${timestamp} 156 | • Author: ${author} 157 | • Content: ${content.trim()}`; 158 | } 159 | }); 160 | } 161 | } 162 | 163 | formattedInteraction += '\n=== END OF SUMMARY ==='; 164 | 165 | Logger.info('Successfully formatted interaction summary'); 166 | 167 | return { 168 | formattedString: formattedInteraction, 169 | userId: userId 170 | }; 171 | 172 | } catch (error) { 173 | Logger.error('Unexpected error in linkTwitterInteractions:', error); 174 | return null; 175 | } 176 | } 177 | 178 | function getResponseType(response: any): string { 179 | const types: string[] = []; 180 | 181 | if (response.in_reply_to_tweet_id) types.push('Reply'); 182 | if (response.retweeted_tweet_id) types.push('Retweet'); 183 | if (response.quoted_tweet_id) types.push('Quote'); 184 | 185 | return types.length ? `${types.join(' + ')}` : 'Main Tweet'; 186 | } -------------------------------------------------------------------------------- /src/supabase/functions/memory/summaries.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | import { formatTimestamp } from '../../../utils/formatTimestamps'; 4 | 5 | interface MemorySummary { 6 | id: number; 7 | summary_type: 'short' | 'mid' | 'long'; 8 | summary: string; 9 | processed: boolean; 10 | session_id: string | null; 11 | created_at?: string; 12 | last_updated?: string; 13 | } 14 | 15 | export class MemorySummaries { 16 | static async saveSummary( 17 | summaryType: 'short' | 'mid' | 'long', 18 | summary: string, 19 | sessionId: string | null 20 | ): Promise { 21 | try { 22 | const { data, error } = await supabase 23 | .from('memory_summaries') 24 | .insert({ 25 | summary_type: summaryType, 26 | summary, 27 | session_id: sessionId, 28 | processed: false 29 | }) 30 | .select(); 31 | 32 | if (error) { 33 | Logger.error(`Error saving ${summaryType}-term summary:`, error); 34 | } else { 35 | Logger.info(`${summaryType}-term summary saved successfully.`, data); 36 | } 37 | } catch (error) { 38 | Logger.error(`Exception in saveSummary when saving ${summaryType}-term summary:`, error); 39 | } 40 | } 41 | 42 | static async markSummariesAsProcessed(summaryIds: number[]): Promise { 43 | try { 44 | await supabase 45 | .from('memory_summaries') 46 | .update({ processed: true }) 47 | .in('id', summaryIds); 48 | } catch (error) { 49 | Logger.error('Error marking summaries as processed:', error); 50 | } 51 | } 52 | 53 | static async updateLongTermSummary(summary: string): Promise { 54 | try { 55 | const { data: currentLongTerm } = await supabase 56 | .from('memory_summaries') 57 | .select('*') 58 | .eq('summary_type', 'long') 59 | .eq('processed', false) 60 | .single(); 61 | 62 | if (currentLongTerm) { 63 | await supabase 64 | .from('memory_summaries') 65 | .update({ processed: true }) 66 | .eq('id', currentLongTerm.id); 67 | } 68 | 69 | await supabase 70 | .from('memory_summaries') 71 | .insert({ 72 | summary_type: 'long', 73 | summary, 74 | session_id: null, 75 | processed: false 76 | }); 77 | } catch (error) { 78 | Logger.error('Error updating long-term summary:', error); 79 | } 80 | } 81 | 82 | static async getActiveMemories(): Promise<{ 83 | short: MemorySummary[]; 84 | mid: MemorySummary[]; 85 | long: MemorySummary | null; 86 | }> { 87 | try { 88 | const { data: shortTerm = [] } = await supabase 89 | .from('memory_summaries') 90 | .select('*') 91 | .eq('summary_type', 'short') 92 | .eq('processed', false) 93 | .order('created_at', { ascending: false }) 94 | .limit(3); 95 | 96 | const { data: midTerm = [] } = await supabase 97 | .from('memory_summaries') 98 | .select('*') 99 | .eq('summary_type', 'mid') 100 | .eq('processed', false) 101 | .order('created_at', { ascending: false }) 102 | .limit(2); 103 | 104 | const { data: longTerm } = await supabase 105 | .from('memory_summaries') 106 | .select('*') 107 | .eq('summary_type', 'long') 108 | .eq('processed', false) 109 | .order('created_at', { ascending: false }) 110 | .limit(1) 111 | .single(); 112 | 113 | return { 114 | short: shortTerm as MemorySummary[], 115 | mid: midTerm as MemorySummary[], 116 | long: longTerm as MemorySummary 117 | }; 118 | } catch (error) { 119 | Logger.error('Error getting active memories:', error); 120 | return { 121 | short: [], 122 | mid: [], 123 | long: null 124 | }; 125 | } 126 | } 127 | 128 | static async checkAndProcessShortTermSummaries(): Promise { 129 | try { 130 | const shortTerms = await this.getUnprocessedSummaries('short', 6); 131 | return shortTerms.length >= 6; 132 | } catch (error) { 133 | Logger.error('Error checking short-term summaries:', error); 134 | return false; 135 | } 136 | } 137 | 138 | static async checkAndProcessMidTermSummaries(): Promise { 139 | try { 140 | const midTerms = await this.getUnprocessedSummaries('mid', 3); 141 | return midTerms.length >= 3; 142 | } catch (error) { 143 | Logger.error('Error checking mid-term summaries:', error); 144 | return false; 145 | } 146 | } 147 | 148 | static async getUnprocessedSummaries( 149 | summaryType: 'short' | 'mid' | 'long', 150 | limit: number 151 | ): Promise { 152 | try { 153 | const { data } = await supabase 154 | .from('memory_summaries') 155 | .select('*') 156 | .eq('summary_type', summaryType) 157 | .eq('processed', false) 158 | .order('created_at', { ascending: true }) 159 | .limit(limit); 160 | 161 | return (data || []) as MemorySummary[]; 162 | } catch (error) { 163 | Logger.error('Error getting unprocessed summaries:', error); 164 | return []; 165 | } 166 | } 167 | 168 | static async getFormattedActiveSummaries(): Promise { 169 | try { 170 | const activeMemories = await this.getActiveMemories(); 171 | 172 | const formattedSummaries: string[] = []; 173 | 174 | const formatSupabaseTimestamp = (timestamp: string | null | undefined): string => { 175 | try { 176 | if (!timestamp) return 'No timestamp'; 177 | const cleanTimestamp = timestamp.split('+')[0] + 'Z'; 178 | return formatTimestamp(new Date(cleanTimestamp)); 179 | } catch (err) { 180 | Logger.error('Error formatting timestamp:', err); 181 | return 'Invalid timestamp'; 182 | } 183 | }; 184 | 185 | if (activeMemories.long) { 186 | const timestamp = formatSupabaseTimestamp(activeMemories.long.created_at); 187 | formattedSummaries.push( 188 | `### LONG TERM SUMMARY\n[${timestamp}]\n${activeMemories.long.summary}\n` 189 | ); 190 | } 191 | 192 | if (activeMemories.mid.length > 0) { 193 | formattedSummaries.push('### MID-TERM SUMMARIES'); 194 | activeMemories.mid 195 | .sort((a, b) => new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime()) 196 | .forEach(summary => { 197 | const timestamp = formatSupabaseTimestamp(summary.created_at); 198 | formattedSummaries.push(`[${timestamp}]\n${summary.summary}\n`); 199 | }); 200 | } 201 | 202 | if (activeMemories.short.length > 0) { 203 | formattedSummaries.push('### SHORT-TERM SUMMARIES'); 204 | activeMemories.short 205 | .sort((a, b) => new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime()) 206 | .forEach(summary => { 207 | const timestamp = formatSupabaseTimestamp(summary.created_at); 208 | formattedSummaries.push(`[${timestamp}]\n${summary.summary}\n`); 209 | }); 210 | } 211 | 212 | const result = formattedSummaries.join('\n'); 213 | 214 | return result || 'No active summaries found.'; 215 | } catch (error) { 216 | Logger.error('Error getting formatted active summaries:', error); 217 | return 'Error retrieving summaries.'; 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /src/supabase/functions/twitter/cooldowns.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../../supabaseClient'; 2 | import { Logger } from '../../../utils/logger'; 3 | 4 | type TweetType = 'main' | 'quote' | 'retweet' | 'media'; 5 | 6 | const COOLDOWN_DURATION = 60; 7 | 8 | function parseTimestampToUTC(timestamp: string): Date { 9 | const formattedTimestamp = timestamp.replace(' ', 'T') + 'Z'; 10 | return new Date(formattedTimestamp); 11 | } 12 | 13 | interface TweetRecord { 14 | created_at: Date; 15 | text: string; 16 | has_media: boolean; 17 | } 18 | 19 | async function getLastTweetDetails(tweetType: TweetType): Promise { 20 | const { data, error } = await supabase 21 | .from('twitter_tweets') 22 | .select('created_at, text, has_media') 23 | .eq('tweet_type', tweetType) 24 | .order('created_at', { ascending: false }) 25 | .limit(1) 26 | .single(); 27 | 28 | if (error) { 29 | Logger.error(`Error fetching last tweet details for type ${tweetType}:`, error.message); 30 | return null; 31 | } 32 | 33 | if (data && data.created_at) { 34 | const createdAtUTC = parseTimestampToUTC(data.created_at); 35 | return { 36 | created_at: createdAtUTC, 37 | text: data.text || '', 38 | has_media: data.has_media || false 39 | }; 40 | } else { 41 | return null; 42 | } 43 | } 44 | 45 | function logCooldownCheck( 46 | tweetType: TweetType, 47 | lastTweetTime: Date | null, 48 | currentTime: Date, 49 | timeSinceLastTweet: number, 50 | cooldownPeriod: number, 51 | text: string | null | undefined 52 | ) { 53 | Logger.info(`\n=== Cooldown Check for ${tweetType.toUpperCase()} tweet ===`); 54 | Logger.info(`Last Tweet Time (UTC): ${lastTweetTime?.toISOString() || 'No previous tweet'}`); 55 | Logger.info(`Current Time (UTC): ${currentTime.toISOString()}`); 56 | Logger.info(`Time Since Last Tweet (ms): ${timeSinceLastTweet}`); 57 | Logger.info(`Cooldown Period (ms): ${cooldownPeriod}`); 58 | if (text) Logger.info(`Last Tweet Text: ${text}`); 59 | Logger.info(`Time remaining: ${Math.max(0, (cooldownPeriod - timeSinceLastTweet) / (60 * 1000)).toFixed(2)} minutes`); 60 | Logger.info(`=====================================\n`); 61 | } 62 | 63 | export async function isCooldownActive(tweetType: TweetType): Promise<{ isActive: boolean; remainingTime: number | null }> { 64 | if (tweetType === 'media') { 65 | Logger.info("\n🖼️ Checking MEDIA tweet cooldown..."); 66 | const [mediaLastTweet, mainWithMediaResult] = await Promise.all([ 67 | getLastTweetDetails('media'), 68 | supabase 69 | .from('twitter_tweets') 70 | .select('created_at, text, has_media') 71 | .eq('tweet_type', 'main') 72 | .eq('has_media', true) 73 | .order('created_at', { ascending: false }) 74 | .limit(1) 75 | .single() 76 | .then(({ data }) => { 77 | Logger.info("Main tweet with media query result:", data); 78 | return data ? { 79 | created_at: parseTimestampToUTC(data.created_at || new Date().toISOString()), 80 | text: data.text || '', 81 | has_media: true 82 | } : null; 83 | }) 84 | ]); 85 | 86 | Logger.info("Dedicated media tweet:", mediaLastTweet); 87 | Logger.info("Main tweet with media:", mainWithMediaResult); 88 | 89 | let lastMediaTweet: TweetRecord | null = null; 90 | if (mediaLastTweet && mainWithMediaResult) { 91 | lastMediaTweet = mainWithMediaResult.created_at > mediaLastTweet.created_at ? mainWithMediaResult : mediaLastTweet; 92 | } else { 93 | lastMediaTweet = mediaLastTweet || mainWithMediaResult; 94 | } 95 | 96 | if (!lastMediaTweet) { 97 | return { isActive: false, remainingTime: null }; 98 | } 99 | 100 | const currentTime = new Date(); 101 | const timeSinceLastTweet = currentTime.getTime() - lastMediaTweet.created_at.getTime(); 102 | const cooldownPeriod = COOLDOWN_DURATION * 60 * 1000; 103 | 104 | const isActive = timeSinceLastTweet < cooldownPeriod; 105 | const remainingTime = isActive ? Math.ceil((cooldownPeriod - timeSinceLastTweet) / (60 * 1000)) : null; 106 | 107 | return { isActive, remainingTime }; 108 | } 109 | 110 | if (tweetType === 'main') { 111 | Logger.info("\n📝 Checking MAIN tweet cooldown..."); 112 | const { data, error } = await supabase 113 | .from('twitter_tweets') 114 | .select('created_at, text, has_media') 115 | .eq('tweet_type', 'main') 116 | .eq('has_media', false) 117 | .order('created_at', { ascending: false }) 118 | .limit(1) 119 | .single(); 120 | 121 | Logger.info("Main tweet query result:", data); 122 | if (error) Logger.error("Error fetching main tweet:", error.message); 123 | 124 | if (!data) { 125 | Logger.info("No previous main tweets found"); 126 | return { isActive: false, remainingTime: null }; 127 | } 128 | 129 | const lastTweetTime = parseTimestampToUTC(data.created_at || new Date().toISOString()); 130 | const currentTime = new Date(); 131 | let timeSinceLastTweet = currentTime.getTime() - lastTweetTime.getTime(); 132 | const cooldownPeriod = COOLDOWN_DURATION * 60 * 1000; 133 | 134 | logCooldownCheck('main', lastTweetTime, currentTime, timeSinceLastTweet, cooldownPeriod, data.text); 135 | 136 | const isActive = timeSinceLastTweet < cooldownPeriod; 137 | const remainingTime = isActive ? Math.ceil((cooldownPeriod - timeSinceLastTweet) / (60 * 1000)) : null; 138 | 139 | return { isActive, remainingTime }; 140 | } 141 | 142 | const lastTweetDetails = await getLastTweetDetails(tweetType); 143 | Logger.info(`lastTweetDetails: ${lastTweetDetails}`); 144 | 145 | if (!lastTweetDetails) { 146 | Logger.info(`No previous tweets of type ${tweetType}. Cooldown not active.`); 147 | return { 148 | isActive: false, 149 | remainingTime: null 150 | }; 151 | } 152 | 153 | const lastTweetTime = lastTweetDetails.created_at; 154 | const currentTime = new Date(); 155 | let timeSinceLastTweet = currentTime.getTime() - lastTweetTime.getTime(); 156 | const cooldownPeriod = COOLDOWN_DURATION * 60 * 1000; 157 | 158 | logCooldownCheck( 159 | tweetType, 160 | lastTweetTime || null, 161 | currentTime, 162 | timeSinceLastTweet, 163 | cooldownPeriod, 164 | lastTweetDetails.text 165 | ); 166 | 167 | if (timeSinceLastTweet < 0) { 168 | Logger.info(`Warning: Last tweet time is in the future. Adjusting timeSinceLastTweet to 0.`); 169 | timeSinceLastTweet = 0; 170 | } 171 | 172 | const isActive = timeSinceLastTweet < cooldownPeriod; 173 | const remainingTime = isActive ? Math.ceil((cooldownPeriod - timeSinceLastTweet) / (60 * 1000)) : null; 174 | 175 | Logger.info(`Cooldown Active: ${isActive}`); 176 | if (isActive) { 177 | Logger.info(`Remaining Cooldown Time (minutes): ${remainingTime}`); 178 | } 179 | 180 | return { 181 | isActive, 182 | remainingTime 183 | }; 184 | } 185 | 186 | export async function getCooldownStatus(): Promise { 187 | const [mainCooldown, quoteCooldown, retweetCooldown, mediaCooldown] = await Promise.all([ 188 | isCooldownActive('main'), 189 | isCooldownActive('quote'), 190 | isCooldownActive('retweet'), 191 | isCooldownActive('media'), 192 | ]); 193 | 194 | return `Tweet Cooldown Status: 195 | Main Tweet: ${mainCooldown.isActive ? `CANNOT SEND A MAIN TWEET. COOLDOWN IS ACTIVE (${mainCooldown.remainingTime} minutes remaining)` : 'CAN SEND A MAIN TWEET. COOLDOWN IS INACTIVE'} 196 | Quote Tweet: ${quoteCooldown.isActive ? `CANNOT SEND A QUOTE TWEET. COOLDOWN IS ACTIVE (${quoteCooldown.remainingTime} minutes remaining)` : 'CAN SEND A QUOTE TWEET. COOLDOWN IS INACTIVE'} 197 | Retweet: ${retweetCooldown.isActive ? `CANNOT RETWEET. COOLDOWN IS ACTIVE (${retweetCooldown.remainingTime} minutes remaining)` : 'CAN RETWEET. COOLDOWN IS INACTIVE'} 198 | Media Tweet: ${mediaCooldown.isActive ? `CANNOT SEND A MEDIA TWEET. COOLDOWN IS ACTIVE (${mediaCooldown.remainingTime} minutes remaining)` : 'CAN SEND A MEDIA TWEET. COOLDOWN IS INACTIVE'}`; 199 | } -------------------------------------------------------------------------------- /src/twitter/utils/extractTweetUtils.ts: -------------------------------------------------------------------------------- 1 | // scripts/extractTweetActions.ts 2 | 3 | import { linkTwitterInteractions, TwitterInteractionResult } from '../../supabase/functions/twitter/linkInteractions'; 4 | import { getShortTermHistory } from '../../supabase/functions/terminal/terminalHistory'; 5 | import { Logger } from '../../utils/logger'; 6 | import { findTwitterUserByUsername } from '../../supabase/functions/twitter/userEntries'; 7 | 8 | interface TweetAction { 9 | sessionId: string; 10 | role: string; 11 | action: string; 12 | tweetId: string; 13 | parentTweetId?: string; 14 | status: string; 15 | details: string; 16 | textContent?: string; 17 | mediaUrls?: string[]; 18 | timestamp?: string; 19 | } 20 | 21 | /** 22 | * Extracts successful tweet actions from the short-term terminal history. 23 | * Supports both single and multiple commands per log entry. 24 | */ 25 | async function extractTweetActions(): Promise { 26 | try { 27 | const messages = await getShortTermHistory(100); 28 | Logger.info(`Retrieved ${messages.length} messages from short-term history`); 29 | 30 | const tweetActions: TweetAction[] = []; 31 | let currentSessionId: string | null = null; 32 | 33 | for (const message of messages) { 34 | if (message.role !== 'user' || !message.content) { 35 | continue; 36 | } 37 | 38 | // Extract timestamp from the log header 39 | const timestampMatch = message.content.match(/\[(\d{2}\/\d{2}\/\d{2} - \d{1,2}:\d{2} [AP]M [A-Z]+)\]/); 40 | const timestamp = timestampMatch ? timestampMatch[1] : null; 41 | 42 | // Split the content into individual command blocks 43 | // Each command block starts with a '$' symbol 44 | const commandBlocks = message.content 45 | .split(/\$(?=\s*[a-zA-Z-]+)/) 46 | .filter((block: string) => block.trim()); 47 | 48 | // Process each command block separately 49 | for (const block of commandBlocks) { 50 | // Clean the command block 51 | const cleanBlock = block.trim(); 52 | 53 | // Skip if not a tweet-related action 54 | if (!cleanBlock.includes('Tweet ID:') && !cleanBlock.includes('Reply Tweet ID:')) { 55 | continue; 56 | } 57 | 58 | // Split into lines and process 59 | const lines = cleanBlock.split('\n'); 60 | 61 | // Extract action details 62 | const actionLine = lines.find((line: string) => line.includes('Action:')); 63 | const parentTweetIdLine = lines.find((line: string) => line.includes('Parent Tweet ID:')); 64 | const replyTweetIdLine = lines.find((line: string) => line.includes('Reply Tweet ID:')); 65 | const tweetIdLine = replyTweetIdLine || parentTweetIdLine; 66 | const statusLine = lines.find((line: string) => line.startsWith('Status:')); 67 | const detailsLine = lines.find((line: string) => line.startsWith('Details:')); 68 | const textLine = lines.find((line: string) => line.startsWith('Text:')); 69 | const mediaLine = lines.find((line: string) => line.startsWith('Media:')); 70 | 71 | // Only process if we have a tweet ID 72 | if (tweetIdLine) { 73 | const tweetId = tweetIdLine.split(':')[1].trim(); 74 | 75 | if (tweetId) { 76 | tweetActions.push({ 77 | sessionId: currentSessionId || 'unknown', 78 | role: message.role, 79 | action: actionLine ? actionLine.replace('Action:', '').replace('��', '').replace('ℹ️', '').trim() : '', 80 | tweetId, 81 | parentTweetId: parentTweetIdLine ? parentTweetIdLine.split(':')[1].trim() : undefined, 82 | status: statusLine ? statusLine.replace('Status:', '').trim() : '', 83 | details: detailsLine ? detailsLine.replace('Details:', '').trim() : '', 84 | textContent: textLine ? textLine.replace('Text:', '').trim() : undefined, 85 | mediaUrls: mediaLine && mediaLine !== 'Media: None' 86 | ? mediaLine.replace('Media:', '').trim().split(', ') 87 | : [], 88 | timestamp: timestamp || undefined 89 | }); 90 | } 91 | } 92 | } 93 | } 94 | 95 | Logger.info(`Extracted ${tweetActions.length} Tweet Actions`); 96 | return tweetActions; 97 | 98 | } catch (error) { 99 | Logger.info('Error in extractTweetActions:', error); 100 | return []; 101 | } 102 | } 103 | 104 | /** 105 | * Gathers all unique user interactions based on tweet actions. 106 | * Groups interactions by user ID to facilitate learning extraction. 107 | */ 108 | export async function gatherUserInteractions(): Promise> { 109 | // Extract tweet actions from the short-term history 110 | const tweetActions = await extractTweetActions(); 111 | 112 | // Collect unique parent tweet IDs from the actions 113 | const uniqueTweetIds = new Set(); 114 | for (const action of tweetActions) { 115 | // Use parentTweetId for reply actions, otherwise use tweetId 116 | const relevantTweetId = action.parentTweetId || action.tweetId; 117 | uniqueTweetIds.add(relevantTweetId); 118 | } 119 | 120 | // Map to group interactions by user ID 121 | const userInteractionsMap = new Map(); 122 | 123 | // Log the tweet IDs we're processing 124 | Logger.info('Processing tweet IDs for interactions:', Array.from(uniqueTweetIds)); 125 | 126 | // Iterate over each unique tweet ID 127 | for (const tweetId of uniqueTweetIds) { 128 | // Retrieve interaction summary and user ID for the tweet 129 | const interactionResult = await linkTwitterInteractions(tweetId); 130 | 131 | if (interactionResult) { 132 | const userId = interactionResult.userId; 133 | 134 | // Initialize array if user ID is encountered for the first time 135 | if (!userInteractionsMap.has(userId)) { 136 | userInteractionsMap.set(userId, []); 137 | } 138 | 139 | // Add the interaction to the user's array of interactions 140 | userInteractionsMap.get(userId)?.push(interactionResult); 141 | } else { 142 | Logger.info(`No interaction found for tweet ID: ${tweetId}`); 143 | } 144 | } 145 | 146 | Logger.info(`Processed interactions for ${userInteractionsMap.size} unique users`); 147 | return userInteractionsMap; 148 | } 149 | 150 | /** 151 | * Formats user interactions into a single comprehensive summary string 152 | * Returns a single string containing all user interactions 153 | */ 154 | export function formatUserInteractions(userInteractionsMap: Map): string { 155 | // Array to store formatted summaries that we'll join later 156 | const formattedSummaries: string[] = []; 157 | 158 | // Iterate through each user's interactions 159 | userInteractionsMap.forEach((interactions, userId) => { 160 | let userSummary = `[USER ID: ${userId}]\n\n`; 161 | 162 | // Add each interaction for this user as a numbered tweet 163 | interactions.forEach((interaction, index) => { 164 | userSummary += `[TWEET ${index + 1}]\n${interaction.formattedString}\n\n`; 165 | }); 166 | 167 | formattedSummaries.push(userSummary); 168 | }); 169 | 170 | // Join all summaries with double newlines between them 171 | return formattedSummaries.join('\n'); 172 | } 173 | 174 | // Usage example: 175 | export async function getFormattedInteractionSummary(): Promise { 176 | const interactions = await gatherUserInteractions(); 177 | return formatUserInteractions(interactions); 178 | } 179 | 180 | /** 181 | * Retrieves user IDs for an array of Twitter usernames from the database 182 | * @param usernames Array of Twitter usernames to look up 183 | * @returns Object mapping usernames to their user IDs (null if not found) 184 | */ 185 | export async function getUserIDsFromUsernames( 186 | usernames: string[] 187 | ): Promise> { 188 | const results: Record = {}; 189 | 190 | // Fetch user IDs concurrently for efficiency 191 | await Promise.all( 192 | usernames.map(async (username) => { 193 | try { 194 | const result = await findTwitterUserByUsername(username); 195 | results[username] = result?.userId || null; 196 | } catch (error) { 197 | Logger.info(`Error retrieving user ID for ${username}:`, error); 198 | results[username] = null; 199 | } 200 | }) 201 | ); 202 | 203 | return results; 204 | } --------------------------------------------------------------------------------