From ade31317e4b3e3d62d9bce9ec3439f1bd9c2a007 Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:33:21 +0700 Subject: [PATCH 1/9] Add SignalShield Analyst submission files Add SignalShield Analyst submission files --- .gitignore | 89 +------ README.md | 715 +++++--------------------------------------------- go.mod | 48 ++-- go.sum | 264 +++++++++++++++++++ main.go | 224 ++++++++++++++++ submission.md | 13 + 6 files changed, 597 insertions(+), 756 deletions(-) create mode 100644 go.sum create mode 100644 main.go create mode 100644 submission.md diff --git a/.gitignore b/.gitignore index 8f34d6a..4389ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,82 +1,13 @@ -# Environment files .env -.env.local -.env.*.local - -# Go build artifacts -*.exe -*.exe~ -*.dll -*.so -*.dylib -*.test -*.out -/dist/ -/build/ - -# compiled binaries (specific) -examples/enhanced-agent/enhanced-agent-example -examples/openai-agent/openai-agent - -# Go module cache -go.sum - -# IDE and editor files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Logs *.log -logs/ -*.log.* - -# Temporary files -*.tmp -*.temp -/tmp/ - -# Private keys and certificates -*.key -*.pem -*.crt -*.p12 -*.pfx - -# Database files *.db -*.sqlite -*.sqlite3 - -# Coverage reports -coverage.out -coverage.html - -# Vendor directory (if using) -/vendor/ - -# Local configuration -config.local.* -settings.local.* - -# Node modules (if any frontend tooling) -node_modules/ - -# Backup files -*.bak -*.backup - -# Test artifacts -testdata/output/ -*.test.json \ No newline at end of file +run.log +run_log.txt +alerts.log +detections.log +.DS_Store +.*.bak +modules_backup_*/ +modules.bak/ +bin/ +build/ \ No newline at end of file diff --git a/README.md b/README.md index 6a254b8..5df799a 100644 --- a/README.md +++ b/README.md @@ -1,655 +1,60 @@ -# Teneo Agent SDK - -Build autonomous agents for the Teneo Network in Go. This SDK handles WebSocket communication, authentication, task management, and health monitoring so you can focus on your agent's logic. - -[![GoLang](https://img.shields.io/badge/golang-00ADD8?&style=plastic&logo=go&logoColor=white)]([https://www.typescriptlang.org/](https://go.dev/)) -[![Version](https://img.shields.io/badge/version%201.0.0-8A2BE2)](https://img.shields.io/badge/version%201.0.0-8A2BE2) - - -## What You Can Build -- **AI Agents**: Connect GPT-5 or other LLMs to the Teneo network in ~15 lines of code -- **Command Agents**: Build agents that respond to specific commands and tasks -- **Custom Agents**: Implement any logic you want - API integrations, data processing, blockchain interactions - -The SDK provides production-ready networking, authentication with Ethereum wallets, automatic reconnection, and built-in health endpoints. - -## Requirements - -- **Go 1.24 or later** -- **Ethereum private key** for network authentication -- **Agent NFT** - automatically minted on first run, or mint via [Teneo Deploy Platform](https://deploy.teneo-protocol.ai) -- **(Optional) OpenAI API key** for AI-powered agents - -## Quickstart - -> [!TIP] -> **Video Tutorial Available!** Watch our step-by-step guide on how to mint your NFT, build your agent, and connect it to the Teneo Agents Chatroom: [Teneo Protocol Agent SDK Set-Up Demo](https://youtu.be/8oqV5tuBthQ?si=gD43iLDeMg1V2zTY) - -### 1. Get SDK -> [!IMPORTANT] -> For the early stage of Teneo Agent SDK use the cloning repository flow (private repository). - -```bash -# Add to your project (when repository is public) -go get github.com/TeneoProtocolAI/teneo-agent-sdk -``` - -#### Using with Private Repository (VM/Development) - -If you're working with the SDK and the repository is still private, clone the SDK locally and use a replace directive: - -```bash -# Clone the SDK to your workspace -git clone https://github.com/TeneoProtocolAI/teneo-agent-sdk.git -cd your-agent-project -``` - -In your `go.mod`, add: - -```go -require ( - github.com/TeneoProtocolAI/teneo-agent-sdk v0.1.0 // Use appropriate version -) - -// Point to local clone -replace github.com/TeneoProtocolAI/teneo-agent-sdk => ./teneo-agent-sdk -``` - -Then run `go mod tidy` to download dependencies. - - -### 2. Configure Environment - -Create a `.env` file: - -```bash -# Required -PRIVATE_KEY=your_ethereum_private_key_without_0x - -NFT_TOKEN_ID=your_token_id_here - -OWNER_ADDRESS=your_wallet_address - -# Optional: Rate limiting (tasks per minute, 0 = unlimited) -RATE_LIMIT_PER_MINUTE=60 -``` - - -### šŸ›‘ BEFORE RUNNING YOUR AGENT: ā›ļø MINT YOUR NFT - -Every agent on the Teneo network requires an NFT that serves as its digital identity and credential. - -#### Mint via Deploy Platform -Visit **[deploy.teneo-protocol.ai](https://deploy.teneo-protocol.ai)** and follow the guided minting process: - -1. Connect your wallet (the same one whose private key you'll use in the SDK) -2. Fill in your agent details (name, description, capabilities) -3. Complete the minting transaction -4. Copy your NFT Token ID -5. Add it to your `.env` file: - ```bash - NFT_TOKEN_ID=your_token_id_here - ``` - ------ -### 3. Run Agent - -The SDK includes ready-to-run examples: - -#### Example 1: Custom Agent - -Build an agent using your own logic. -Open the [Teneo Deploy Platform](https://deploy.teneo-protocol.ai) , fill out the form, and when you're ready, mint the NFT. -Use the ready-to-use code snippet generated based on your inputs. - -Alternatively, you can use this simple command processor: - -```go -package main - -import ( - "context" - "fmt" - "log" - "os" - "strings" - "time" - - "github.com/TeneoProtocolAI/teneo-agent-sdk/pkg/agent" -) - -type CommandAgent struct{} - -func (a *CommandAgent) ProcessTask(ctx context.Context, task string) (string, error) { - log.Printf("Processing task: %s", task) - - // Clean up the task input - task = strings.TrimSpace(task) - task = strings.TrimPrefix(task, "/") - taskLower := strings.ToLower(task) - - // Split into command and arguments - parts := strings.Fields(taskLower) - if len(parts) == 0 { - return "No command provided.", nil - } - - command := parts[0] - args := parts[1:] - - // Route to appropriate command handler - switch command { - case "comman_1": - // Command Logic - return "command_1 executed" - - default: - return fmt.Sprintf("Unknown command '%s'", command), nil - } -} - -func main() { - config := agent.DefaultConfig() - config.Name = "My Command Agent" - config.Description = "Handles time, weather, and greetings" - config.Capabilities = []string{"time", "weather", "greetings"} - config.PrivateKey = os.Getenv("PRIVATE_KEY") - config.NFTTokenID = os.Getenv("NFT_TOKEN_ID") - config.OwnerAddress = os.Getenv("OWNER_ADDRESS") - - enhancedAgent, err := agent.NewEnhancedAgent(&agent.EnhancedAgentConfig{ - Config: config, - AgentHandler: &CommandAgent{}, - }) - - if err != nil { - log.Fatal(err) - } - - log.Println("Starting agent...") - enhancedAgent.Run() -} -``` - -and run the Agent: - -```bash -go mod tidy - -# Run the agent -go run main.go -``` - ----- -#### Example 1: GPT-5 Agent (Simplest - Start Here) -To correctly run the first example, add your OpenAI API key to `.env` file: - -```bash -# Set your keys in .env -OPENAI_API_KEY=sk-your_openai_key -``` - -and run the Agent: - -```bash -cd examples/openai-agent - -go mod tidy - -# Run the agent -go run main.go -``` - -**That's it!** -Your AI agent is now live on the Teneo Test network, powered by GPT-5. - ----- - -## Where Your Agent is Deployed - -Once your agent is running, it is automatically deployed to the [**Developers Chatroom**](https://developer.chatroom.teneo-protocol.ai/chatroom) application. - -### Visibility Settings - -- **By Default**: Your agent is visible only to you (the owner) -- **Making it Public**: To make your agent available to other users: - 1. Go to [**My Agents**](https://deploy.teneo-protocol.ai/my-agents) page - 2. Switch the visibility button to public - 3. Your agent will go through a verification process - 4. Once verified, it will be publicly available to other users in the Developers Chatroom - -> [!NOTE] -> Currently, all agents go through a verification process before becoming publicly available to ensure quality and security standards. - ----- -### Agent Interface - -Every agent implements this simple interface: - -```go -type AgentHandler interface { - ProcessTask(ctx context.Context, task string) (string, error) -} -``` - -That's it. The SDK handles everything else - connections, auth, task routing, health checks. - -### Optional Interfaces - -Add these for more control: - -```go -// Initialize resources when agent starts -type AgentInitializer interface { - Initialize(ctx context.Context, config interface{}) error -} - -// Clean up when agent stops -type AgentCleaner interface { - Cleanup(ctx context.Context) error -} - -// Handle task results for logging/analytics -type TaskResultHandler interface { - HandleTaskResult(ctx context.Context, taskID, result string) error -} -``` - -### Agent Types - -**SimpleOpenAIAgent** - The easiest option. Just provide your OpenAI key and you're done. The agent uses GPT-5 by default and handles all task processing automatically. - -**EnhancedAgent** - For custom logic. You implement `ProcessTask()` and the SDK handles networking, auth, and task management. Use this when you want full control over how your agent responds. - -**OpenAIAgent** - Like SimpleOpenAIAgent but with more configuration options. Customize the model, temperature, system prompt, and streaming behavior. - -### Programmatic Configuration - -```go -config := agent.DefaultConfig() - -// Basic info -config.Name = "Weather Agent" -config.Description = "Provides weather information" -config.Capabilities = []string{"weather", "forecast", "temperature"} - -// Network (optional - defaults to production endpoints) -config.Room = "weather-agents" // Join a specific room - -// Performance -config.MaxConcurrentTasks = 10 -config.TaskTimeout = 60 // seconds - -// Rate limiting (0 = unlimited) -config.RateLimitPerMinute = 60 // Limit to 60 tasks per minute - -// Health monitoring -config.HealthEnabled = true -config.HealthPort = 8080 - -// Authentication (required) -config.PrivateKey = os.Getenv("PRIVATE_KEY") -``` - -## Customizing OpenAI Agents - -The OpenAI integration is highly configurable: - -```go -agent, err := agent.NewSimpleOpenAIAgent(&agent.SimpleOpenAIAgentConfig{ - PrivateKey: os.Getenv("PRIVATE_KEY"), - OpenAIKey: os.Getenv("OPENAI_API_KEY"), - - // Customize behavior - Name: "Customer Support AI", - Description: "Handles customer inquiries 24/7", - Model: "gpt-5", - Temperature: 0.7, - MaxTokens: 1500, - Streaming: false, - - SystemPrompt: `You are a professional customer support agent. -Be helpful, friendly, and solution-oriented. -Keep responses clear and concise.`, - - Capabilities: []string{"support", "troubleshooting", "inquiries"}, - - // Optional: Join a specific room - Room: "support", - - // Optional: Rate limiting to manage costs - RateLimitPerMinute: 30, // Max 30 requests/minute -}) -``` - -## Health Monitoring - -The SDK provides HTTP endpoints automatically: - -```bash -# Check if agent is alive -curl http://localhost:8080/health - -# Get detailed status -curl http://localhost:8080/status - -# Get agent info -curl http://localhost:8080/info -``` - -Example response: - -```json -{ - "status": "operational", - "connected": true, - "authenticated": true, - "active_tasks": 3, - "uptime": "1h23m15s", - "agent": { - "name": "My Agent", - "version": "1.0.0", - "wallet": "0x742d35Cc6570E952BE...", - "capabilities": ["weather", "time"] - } -} -``` - -## Rate Limiting - -The SDK supports rate limiting to control the number of tasks processed per minute. This helps prevent overload and manage costs for AI-powered agents. - -### Configuration - -Set via environment variable: - -```bash -# Limit to 60 tasks per minute -RATE_LIMIT_PER_MINUTE=60 - -# Unlimited (default) -RATE_LIMIT_PER_MINUTE=0 -``` - -Or programmatically: - -```go -config := agent.DefaultConfig() -config.RateLimitPerMinute = 60 // Limit to 60 tasks per minute -``` - -### Behavior - -When the rate limit is exceeded: -- Users receive: "āš ļø Agent rate limit exceeded. This agent has reached its maximum request capacity. Please try again in a moment." -- Error code: `rate_limit_exceeded` -- The task is automatically rejected without processing - -### Implementation Details - -- Uses a **sliding window** approach tracking requests over the past minute -- **Thread-safe** with mutex locks for concurrent operations -- Applies to both incoming tasks and user messages -- Value of `0` means unlimited (no rate limiting) - -## Persistent Caching with Redis - -The SDK includes built-in Redis support for persistent data storage across agent restarts. This enables stateful agents that can cache results, maintain session data, and coordinate across multiple instances. - -### Quick Start - -**1. Start Redis:** -```bash -docker run -d -p 6379:6379 redis:latest -``` - -**2. Enable in your `.env`:** -```bash -REDIS_ENABLED=true -REDIS_ADDRESS=localhost:6379 -``` - -**3. Use in your agent:** -```go -type MyAgent struct { - cache cache.AgentCache -} - -func (a *MyAgent) Initialize(ctx context.Context, config interface{}) error { - if ea, ok := config.(*agent.EnhancedAgent); ok { - a.cache = ea.GetCache() - } - return nil -} - -func (a *MyAgent) ProcessTask(ctx context.Context, task string) (string, error) { - // Check cache first - cached, err := a.cache.Get(ctx, "task:"+task) - if err == nil { - return cached, nil // Cache hit - } - - // Process task - result := processTask(task) - - // Cache for 5 minutes - a.cache.Set(ctx, "task:"+task, result, 5*time.Minute) - - return result, nil -} -``` - -### Features - -- āœ… **Automatic key prefixing** - No collisions between agents -- āœ… **Graceful degradation** - Agent works without Redis -- āœ… **TTL support** - Automatic expiration of cached data -- āœ… **Rich API** - Set, Get, Increment, Locks, Pattern deletion -- āœ… **Type-safe** - Supports strings, bytes, and JSON -- āœ… **Production-ready** - Connection pooling, retries, timeouts - -### Configuration Options - -| Environment Variable | Description | Default | -|---------------------|-------------|---------| -| `REDIS_ENABLED` | Enable Redis caching | `false` | -| `REDIS_ADDRESS` | Redis server address (host:port) | `localhost:6379` | -| `REDIS_USERNAME` | Redis ACL username (Redis 6+) | `""` | -| `REDIS_PASSWORD` | Redis password | `""` | -| `REDIS_USE_TLS` | Enable TLS/SSL connection | `false` | -| `REDIS_DB` | Database number (0-15) | `0` | -| `REDIS_KEY_PREFIX` | Custom key prefix | `teneo:agent::` | - -**Local Redis:** -```bash -REDIS_ENABLED=true -REDIS_ADDRESS=localhost:6379 -``` - -**Managed Redis (DigitalOcean, AWS, etc.):** -```bash -REDIS_ENABLED=true -REDIS_ADDRESS=your-redis-host.com:25061 -REDIS_USERNAME=default -REDIS_PASSWORD=your-password -REDIS_USE_TLS=true -``` - -Or configure programmatically: -```go -config := agent.DefaultConfig() -config.RedisEnabled = true -config.RedisAddress = "redis.example.com:6379" -config.RedisUsername = "agentuser" // Redis 6+ ACL username -config.RedisPassword = "secret" -config.RedisUseTLS = true // For managed Redis -``` - -### Common Use Cases - -**Cache API Responses:** -```go -// Avoid redundant API calls -data, err := a.cache.Get(ctx, "api:user:123") -if err != nil { - data = fetchFromAPI("123") - a.cache.Set(ctx, "api:user:123", data, 10*time.Minute) -} -``` - -**Distributed Rate Limiting:** -```go -// Share rate limits across agent instances -count, _ := a.cache.Increment(ctx, "ratelimit:user:"+userID) -if count > 100 { - return errors.New("rate limit exceeded") -} -``` - -**Session Management:** -```go -// Persist sessions across restarts -a.cache.Set(ctx, "session:"+id, sessionData, 24*time.Hour) -``` - -**Distributed Locks:** -```go -// Coordinate across multiple instances -acquired, _ := a.cache.SetIfNotExists(ctx, "lock:resource", "1", 30*time.Second) -if !acquired { - return errors.New("resource locked") -} -``` - -### Full Documentation - -- **[Redis Cache Guide](docs/REDIS_CACHE.md)** - Complete API reference and examples - -## Advanced Features - -### Streaming and Multi-Message Tasks - -For long-running tasks, send multiple messages as you process: - -```go -type StreamingAgent struct{} - -func (a *StreamingAgent) ProcessTaskWithStreaming(ctx context.Context, task string, sender types.MessageSender) error { - // Send initial acknowledgment - sender.SendMessage("Starting analysis...") - - // Do some work - time.Sleep(1 * time.Second) - sender.SendTaskUpdate("Step 1 complete") - - // More work - time.Sleep(1 * time.Second) - sender.SendTaskUpdate("Step 2 complete") - - // Final result - return sender.SendMessage("Analysis complete! Here are the results...") -} -``` - -``` - -### Runtime Updates - -Update agent capabilities while running: - -```go -coordinator := enhancedAgent.GetTaskCoordinator() -coordinator.UpdateCapabilities([]string{"new_capability", "updated_feature"}) -``` - -### Custom Authentication - -Access the auth manager for signing: - -```go -authManager := enhancedAgent.GetAuthManager() -address := authManager.GetAddress() -signature, err := authManager.SignMessage("custom message") -``` - -## Error Handling - -The SDK handles reconnection automatically, but you should still handle errors in your agent logic: - -```go -func (a *MyAgent) ProcessTask(ctx context.Context, task string) (string, error) { - result, err := a.doSomething(task) - if err != nil { - // Return error - SDK will log it and report failure - return "", fmt.Errorf("failed to process: %w", err) - } - - // Check context cancellation for long tasks - select { - case <-ctx.Done(): - return "", ctx.Err() - default: - return result, nil - } -} -``` - -## Troubleshooting - -**Connection issues** -``` -Failed to connect to WebSocket -``` -- The SDK uses production endpoints by default - ensure the Teneo network is operational -- If you've overridden `WEBSOCKET_URL`, verify it's correct -- Check your internet connection and firewall settings - -**Authentication failed** -``` -Authentication failed: invalid signature -``` -- Verify `PRIVATE_KEY` is valid (remove `0x` prefix if present) -- Ensure the wallet is authorized on the network -- Check that the private key matches the expected format - -**OpenAI errors** -``` -OpenAI API error: insufficient credits -``` -- Check your OpenAI account has available credits -- Verify the API key is valid and active -- Ensure the model name is correct (e.g., `gpt-5`, not `gpt5`) - -**Task timeouts** -``` -Task timeout after 30 seconds -``` -- Increase `TaskTimeout` in your config -- Optimize your `ProcessTask` implementation -- Check for blocking operations or infinite loops - -Enable debug logging: - -```bash -export LOG_LEVEL=debug -go run main.go -``` - -## Vibe Coding -- [Wrapping Your Business Logic](docs/WRAPPING_BUSINESS_LOGIC.md) - Use Claude Code to automatically integrate your code -- [Running with NFTs](docs/RUNNING_WITH_NFT.md) - NFT integration guide -- [Examples](examples/) - Complete working examples - -## License - -Teneo-Agent-SDK is open source under the [AGPL-3.0 license](LICENCE). - -## Support - -- **Discord**: [Join our community](https://discord.com/invite/teneoprotocol) -- **Issues**: [GitHub Issues](https://github.com/TeneoProtocolAI/teneo-agent-sdk/issues) - ---- - -Built by the Teneo team ā¤ļø -Start building your agents today. +# SignalShield Analyst + +SignalShield Analyst is a fast-response crypto market intelligence agent for the Teneo Protocol Chatroom. +It detects early KOL calls, evaluates hype & sentiment, computes risk scores, and provides real-time market metrics powered by CoinGecko and Google Gemini. + +## Features +- Early-call detection (mock/live) +- Hype scoring +- Sentiment analysis +- Risk evaluation +- Trend detection +- Influencer tracking (mock/live) +- Gemini AI reasoning +- Alert system +- Market metrics via CoinGecko (price, volume, marketcap) +- Full Teneo agent lifecycle (auth → register → websocket) + +## Requirements +- Go 1.20+ +- Google API Key (Generative Language API) +- Private key to sign Teneo challenges +- NFT tokenID registered in Teneo +- Internet connection + +## Environment Variables +Create `.env` (never commit): + +PRIVATE_KEY= +NFT_TOKEN_ID=366 +OWNER_ADDRESS=0x... +GOOGLE_API_KEY=AIzaSy... +GOOGLE_MODEL=models/gemini-2.5-flash +COINGECKO_BASE_CURRENCY=https://api.coingecko.com/api/v3 + +MOCK_MODE=true +RATE_LIMIT_PER_MINUTE=30 +ENABLE_FORWARD_OPENAI=false + +## Running +go mod tidy +go run . +Health check: +curl http://localhost:8081/health + +## Supported Commands +@signalshield-analyst hype sol +@signalshield-analyst sentiment eth +@signalshield-analyst riskcheck btc +@signalshield-analyst gecko pepe +@signalshield-analyst ai "explain risks of SOL in 3 bullets" +@signalshield-analyst alert BTC "touch support" + +## Troubleshooting +- API key invalid → re-export env variables +- 404 CoinGecko → symbol not mapped +- Gemini JSON error → malformed request (fixed in current version) + +## License +MIT + diff --git a/go.mod b/go.mod index 0975d3b..e80fd97 100644 --- a/go.mod +++ b/go.mod @@ -1,47 +1,51 @@ -module github.com/TeneoProtocolAI/teneo-agent-sdk +module signalshield -go 1.24.0 - -toolchain go1.24.9 +go 1.25.4 require ( - github.com/ethereum/go-ethereum v1.16.5 - github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/gorilla/websocket v1.5.3 - github.com/redis/go-redis/v9 v9.16.0 - github.com/sashabaranov/go-openai v1.41.2 + github.com/TeneoProtocolAI/teneo-agent-sdk v0.3.0 + github.com/joho/godotenv v1.5.1 + modernc.org/sqlite v1.40.1 ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/StackExchange/wmi v1.2.1 // indirect - github.com/bits-and-blooms/bitset v1.24.1 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.3 // indirect + github.com/ethereum/go-ethereum v1.16.5 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect - github.com/klauspost/compress v1.17.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/redis/go-redis/v9 v9.16.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sashabaranov/go-openai v1.41.2 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/supranational/blst v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) - -// Pin crypto to version compatible with Go 1.21 -replace golang.org/x/crypto => golang.org/x/crypto v0.16.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..44384e8 --- /dev/null +++ b/go.sum @@ -0,0 +1,264 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/TeneoProtocolAI/teneo-agent-sdk v0.3.0 h1:RPY4PHYNvTC+lPvLoX1BUPt6x7JiDDduyLeWwSQP3fM= +github.com/TeneoProtocolAI/teneo-agent-sdk v0.3.0/go.mod h1:Qcwn+3z5iVsOjjO9WOOAvWLwytXTnl2/Wp6G1lCWgMk= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU= +github.com/ethereum/c-kzg-4844/v2 v2.1.3/go.mod h1:fyNcYI/yAuLWJxf4uzVtS8VDKeoAaRM8G/+ADz/pRdA= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.5 h1:GZI995PZkzP7ySCxEFaOPzS8+bd8NldE//1qvQDQpe0= +github.com/ethereum/go-ethereum v1.16.5/go.mod h1:kId9vOtlYg3PZk9VwKbGlQmSACB5ESPTBGT+M9zjmok= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= +github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= +github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= +modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..384bddc --- /dev/null +++ b/main.go @@ -0,0 +1,224 @@ +// main.go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + "signalshield/modules" + + "github.com/TeneoProtocolAI/teneo-agent-sdk/pkg/agent" + "github.com/joho/godotenv" +) + +type SignalshieldAnalystAgent struct{} + +func (a *SignalshieldAnalystAgent) ProcessTask(ctx context.Context, task string) (string, error) { + log.Printf("Processing task: %s", task) + + task = strings.TrimSpace(task) + task = strings.TrimPrefix(task, "/") + parts := strings.Fields(task) + if len(parts) == 0 { + return "No command provided. Available commands: scan, monitor, riskcheck, hype, signal, dumpalert, topcalls, sentiment, watch, summary, marketcap, volume, price, gecko, trend, alert, subscribe, unsubscribe, ai", nil + } + cmd := strings.ToLower(parts[0]) + args := parts[1:] + + switch cmd { + case "scan": + return modules.RunScan(args) + case "monitor": + return "Monitor command (mock): started (use /monitor )", nil + case "riskcheck": + return modules.RunRiskCheck(args) + case "hype": + return modules.RunHype(args) + case "signal": + return "Latest signals: 3 new early calls, 1 dump alert (mock).", nil + case "dumpalert": + return "Dump alert check: no immediate dump signals detected (mock).", nil + case "topcalls": + return modules.RunTopCalls() + case "sentiment": + return modules.RunSentiment(args) + case "watch": + return modules.RunWatch(args) + case "summary": + return modules.RunSummary() + case "marketcap": + if len(args) == 0 { + return "Usage: marketcap [token]", nil + } + return modules.GetMarketCap(strings.Join(args, "")) + case "volume": + if len(args) == 0 { + return "Usage: volume [token]", nil + } + return modules.GetVolume(strings.Join(args, "")) + case "price": + if len(args) == 0 { + return "Usage: price [token]", nil + } + return modules.GetCoinPrice(strings.Join(args, "")) + case "gecko", "geckosnapshot": + if len(args) == 0 { + return "Usage: gecko [id_or_symbol]", nil + } + res, err := modules.GetCoinGeckoFull(strings.Join(args, "")) + if err != nil { + return "", err + } + // FormatCoinGeckoSummary returns string -> must return (string, nil) + return modules.FormatCoinGeckoSummary(res), nil + case "trend": + if len(args) == 0 { + return "Usage: trend [token]", nil + } + // GetTrendSnapshot returns (string, error) so just forward it + return modules.GetTrendSnapshot(strings.Join(args, "")) + case "alert": + return "Alert command (mock): created (use alert [token] [condition])", nil + case "subscribe": + return "Subscribe (mock): done", nil + case "unsubscribe": + return "Unsubscribe (mock): done", nil + case "ai": + // forward natural language instruction to GPT module + if len(args) == 0 { + return "Usage: ai [instruction]", nil + } + instr := strings.Join(args, " ") + // IMPORTANT: ForwardToOpenAI in modules now prioritizes GOOGLE_API_KEY (if set) + if os.Getenv("GOOGLE_API_KEY") == "" && os.Getenv("OPENAI_API_KEY") == "" { + return "AI backend not configured. Set GOOGLE_API_KEY or OPENAI_API_KEY in .env", nil + } + resp, err := modules.ForwardToOpenAI(instr) + if err != nil { + return "", err + } + return resp, nil + default: + return fmt.Sprintf("Unknown command '%s'. Available commands: scan, monitor, riskcheck, hype, signal, dumpalert, topcalls, sentiment, watch, summary, marketcap, volume, price, gecko, trend, alert, subscribe, unsubscribe, ai", cmd), nil + } +} + +func main() { + // Load .env if available + _ = godotenv.Load() + + // Basic config & env + rateLimitStr := os.Getenv("RATE_LIMIT_PER_MINUTE") + rateLimit := 0 + if rateLimitStr != "" { + if v, err := strconv.Atoi(rateLimitStr); err == nil { + rateLimit = v + } + } + + pollInterval := 30 + if s := os.Getenv("X_POLL_INTERVAL"); s != "" { + if v, err := strconv.Atoi(s); err == nil { + pollInterval = v + } + } + kols := []string{"Ansem", "GCR", "TheMoonCarl"} // default + if s := os.Getenv("KOL_LIST"); s != "" { + parts := strings.Split(s, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + if len(parts) > 0 { + kols = parts + } + } + xBearer := os.Getenv("X_BEARER_TOKEN") + source := "mock-x" + mock := true + if os.Getenv("MOCK_MODE") == "false" { + mock = false + } + + // Teneo agent config + _ = godotenv.Load() + config := agent.DefaultConfig() + config.Name = "SignalShield Analyst" + config.Description = "SignalShield Analyst monitors KOL early calls + market signals." + config.Capabilities = []string{"early-call-detection", "risk-mitigation-engine", "sentiment-analysis", "hype-index-scoring", "dump-alert-system", "influencer-tracking", "trend-detection", "anomaly-detection", "multi-chain-token-monitoring", "risk-hype-balancer"} + config.PrivateKey = os.Getenv("PRIVATE_KEY") + config.NFTTokenID = os.Getenv("NFT_TOKEN_ID") + config.OwnerAddress = os.Getenv("OWNER_ADDRESS") + config.RateLimitPerMinute = rateLimit + + enhancedAgent, err := agent.NewEnhancedAgent(&agent.EnhancedAgentConfig{ + Config: config, + AgentHandler: &SignalshieldAnalystAgent{}, + }) + if err != nil { + log.Fatal("agent.NewEnhancedAgent:", err) + } + + log.Println("Starting SignalShield Analyst...") + // run agent in goroutine so we can also start scanner & detection loop + go enhancedAgent.Run() + + // Create context for scanner & detector + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // detection channel + detectCh := make(chan modules.Detection, 16) + + // start scanner (xscanner) + go modules.StartXScanner(ctx, pollInterval, kols, xBearer, source, mock, detectCh) + + // goroutine to handle detections + go func() { + for d := range detectCh { + // Save detection to file (modules.SaveDetection expects Detection) + if err := modules.SaveDetection("alerts.log", d); err != nil { + log.Println("Warning: SaveDetection failed:", err) + } + // optional: forward text to model pipeline for short summary (non-blocking) + go func(det modules.Detection) { + // prefer GOOGLE_API_KEY if set, otherwise OPENAI_API_KEY + if os.Getenv("GOOGLE_API_KEY") == "" && os.Getenv("OPENAI_API_KEY") == "" { + return + } + res, err := modules.ForwardToOpenAI(det.Text) + if err != nil { + log.Println("ForwardToOpenAI err:", err) + return + } + log.Println("[xscanner] GPT summary:", res) + }(d) + } + }() + + // health server (simple) + httpPort := "8080" + if p := os.Getenv("HEALTH_PORT"); p != "" { + httpPort = p + } + // Provide a very small health endpoint (so curl http://localhost:8080/health works) + go func() { + ln := ":" + httpPort + log.Printf("HTTP server listening on :%s", httpPort) + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fmt.Sprintf(`{"agent":"%s","status":"healthy","timestamp":"%s","kols":%q,"mock":%v,"pollSec":%d}`, config.Name, time.Now().UTC().Format(time.RFC3339), kols, mock, pollInterval))) + }) + if err := http.ListenAndServe(ln, nil); err != nil { + log.Println("health server error:", err) + } + }() + + // block forever (agent runs in background) + select {} +} diff --git a/submission.md b/submission.md new file mode 100644 index 0000000..062a240 --- /dev/null +++ b/submission.md @@ -0,0 +1,13 @@ +SignalShield Analyst is a crypto intelligence agent integrating Gemini and CoinGecko. +It supports hype, sentiment, riskcheck, trend, alerts, market data, and AI reasoning. +Fully compatible with Teneo agent lifecycle and WebSocket communication. + +Commands tested: +- hype +- sentiment +- riskcheck +- gecko +- ai "" +- alert "" + +Built in Go, using Gemini v1beta API, CoinGecko price feed, and mock/live scanners. From 9fe8afdf2869d6d9b5740c07583dceb9fb9651f3 Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:35:08 +0700 Subject: [PATCH 2/9] Create modules --- modules | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules diff --git a/modules b/modules new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/modules @@ -0,0 +1 @@ + From 9b485cc216e5837be22eb3a4fb7715659ceca5e8 Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:41:37 +0700 Subject: [PATCH 3/9] Delete modules --- modules | 1 - 1 file changed, 1 deletion(-) delete mode 100644 modules diff --git a/modules b/modules deleted file mode 100644 index 8b13789..0000000 --- a/modules +++ /dev/null @@ -1 +0,0 @@ - From 464d3f17c16c1cadbdd3a43514eec0a90fbc977e Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:43:41 +0700 Subject: [PATCH 4/9] Delete tests directory --- .../coordinator_integration_test.go | 334 ---------------- tests/integration/go.mod | 7 - tests/run_all_tests.sh | 132 ------- tests/unit/agent_naming_test.go | 245 ------------ tests/unit/go.mod | 7 - tests/unit/standardized_messaging_test.go | 360 ------------------ 6 files changed, 1085 deletions(-) delete mode 100644 tests/integration/coordinator_integration_test.go delete mode 100644 tests/integration/go.mod delete mode 100755 tests/run_all_tests.sh delete mode 100644 tests/unit/agent_naming_test.go delete mode 100644 tests/unit/go.mod delete mode 100644 tests/unit/standardized_messaging_test.go diff --git a/tests/integration/coordinator_integration_test.go b/tests/integration/coordinator_integration_test.go deleted file mode 100644 index e03e660..0000000 --- a/tests/integration/coordinator_integration_test.go +++ /dev/null @@ -1,334 +0,0 @@ -package integration - -import ( - "context" - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/TeneoProtocolAI/teneo-agent-sdk/pkg/types" -) - -// MockProtocolHandler simulates the real ProtocolHandler for testing -type MockProtocolHandler struct { - messages []string -} - -func (m *MockProtocolHandler) SendTaskResponseToRoom(taskID, content string, success bool, errorMsg, room string) error { - message := fmt.Sprintf("TaskID: %s, Room: %s, Success: %t, Content: %s", taskID, room, success, content) - m.messages = append(m.messages, message) - - // Parse and validate the standardized message format - var standardizedMsg types.StandardizedMessage - if err := json.Unmarshal([]byte(content), &standardizedMsg); err != nil { - return fmt.Errorf("invalid standardized message format: %w", err) - } - - return nil -} - -func (m *MockProtocolHandler) GetMessages() []string { - return m.messages -} - -// TaskMessageSenderTest creates TaskMessageSender with mock protocol handler -type TaskMessageSenderTest struct { - taskID string - room string - mockProtocol *MockProtocolHandler -} - -func NewTaskMessageSenderTest(taskID, room string) *TaskMessageSenderTest { - mockProtocol := &MockProtocolHandler{messages: make([]string, 0)} - - return &TaskMessageSenderTest{ - taskID: taskID, - room: room, - mockProtocol: mockProtocol, - } -} - -// Simulate TaskMessageSender methods with mock protocol -func (t *TaskMessageSenderTest) SendMessage(content string) error { - return t.sendStandardizedMessage(types.StandardMessageTypeString, content) -} - -func (t *TaskMessageSenderTest) SendTaskUpdate(content string) error { - updateContent := fmt.Sprintf("šŸ”„ Update: %s", content) - return t.sendStandardizedMessage(types.StandardMessageTypeString, updateContent) -} - -func (t *TaskMessageSenderTest) SendMessageAsJSON(content interface{}) error { - return t.sendStandardizedMessage(types.StandardMessageTypeJSON, content) -} - -func (t *TaskMessageSenderTest) SendMessageAsMD(content string) error { - return t.sendStandardizedMessage(types.StandardMessageTypeMD, content) -} - -func (t *TaskMessageSenderTest) SendMessageAsArray(content []interface{}) error { - return t.sendStandardizedMessage(types.StandardMessageTypeArray, content) -} - -func (t *TaskMessageSenderTest) sendStandardizedMessage(msgType string, content interface{}) error { - standardizedMsg := types.StandardizedMessage{ - ContentType: msgType, - Content: content, - } - - contentJSON, err := json.Marshal(standardizedMsg) - if err != nil { - return fmt.Errorf("failed to marshal standardized message: %w", err) - } - - // This simulates what TaskMessageSender.sendStandardizedMessage does - return t.mockProtocol.SendTaskResponseToRoom(t.taskID, string(contentJSON), true, "", t.room) -} - -// IntegrationTestAgent tests with the TaskMessageSender -type IntegrationTestAgent struct{} - -func (a *IntegrationTestAgent) ProcessTask(ctx context.Context, task string) (string, error) { - return "Integration test completed", nil -} - -func (a *IntegrationTestAgent) ProcessTaskWithStreaming(ctx context.Context, task, room string, sender types.MessageSender) error { - // Test the actual methods that would be called - if err := sender.SendMessage("Integration test started"); err != nil { - return fmt.Errorf("string message failed: %w", err) - } - - testData := map[string]interface{}{ - "integration_test": true, - "coordinator": "TaskCoordinator", - "sender": "TaskMessageSender", - "room": room, - "timestamp": time.Now().Format(time.RFC3339), - } - - if err := sender.SendMessageAsJSON(testData); err != nil { - return fmt.Errorf("JSON message failed: %w", err) - } - - markdown := `# Integration Test - -## Components Tested -- āœ… TaskMessageSender -- āœ… ProtocolHandler simulation -- āœ… Standardized message format - -All components working correctly.` - - if err := sender.SendMessageAsMD(markdown); err != nil { - return fmt.Errorf("markdown message failed: %w", err) - } - - results := []interface{}{ - map[string]interface{}{ - "component": "TaskMessageSender", - "status": "working", - "test_time": time.Now().Unix(), - }, - map[string]interface{}{ - "component": "ProtocolHandler", - "status": "working", - "test_time": time.Now().Unix(), - }, - map[string]interface{}{ - "component": "StandardizedFormat", - "status": "working", - "test_time": time.Now().Unix(), - }, - } - - if err := sender.SendMessageAsArray(results); err != nil { - return fmt.Errorf("array message failed: %w", err) - } - - if err := sender.SendTaskUpdate("All integration tests passed"); err != nil { - return fmt.Errorf("task update failed: %w", err) - } - - return nil -} - -func TestTaskMessageSenderIntegration(t *testing.T) { - // Create test components - taskID := "integration-test-001" - room := "integration-room" - sender := NewTaskMessageSenderTest(taskID, room) - agent := &IntegrationTestAgent{} - - // Run integration test - ctx := context.Background() - task := "integration test task" - - err := agent.ProcessTaskWithStreaming(ctx, task, room, sender) - if err != nil { - t.Fatalf("Integration test failed: %v", err) - } - - // Validate results - messages := sender.mockProtocol.GetMessages() - expectedCount := 5 // string, json, markdown, array, update - if len(messages) != expectedCount { - t.Errorf("Expected %d messages, got %d", expectedCount, len(messages)) - } - - // Validate message format - for i, msg := range messages { - if !contains(msg, taskID) { - t.Errorf("Message %d should contain task ID '%s'", i+1, taskID) - } - - if !contains(msg, room) { - t.Errorf("Message %d should contain room '%s'", i+1, room) - } - - if !contains(msg, "Success: true") { - t.Errorf("Message %d should indicate success", i+1) - } - } -} - -func TestStandardizedMessageValidation(t *testing.T) { - sender := NewTaskMessageSenderTest("test", "room") - - // Test each message type - tests := []struct { - name string - sendFunc func() error - expectedType string - }{ - { - name: "string message", - sendFunc: func() error { - return sender.SendMessage("test message") - }, - expectedType: types.StandardMessageTypeString, - }, - { - name: "json message", - sendFunc: func() error { - return sender.SendMessageAsJSON(map[string]interface{}{"key": "value"}) - }, - expectedType: types.StandardMessageTypeJSON, - }, - { - name: "markdown message", - sendFunc: func() error { - return sender.SendMessageAsMD("# Test") - }, - expectedType: types.StandardMessageTypeMD, - }, - { - name: "array message", - sendFunc: func() error { - return sender.SendMessageAsArray([]interface{}{"item1", "item2"}) - }, - expectedType: types.StandardMessageTypeArray, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Clear previous messages - sender.mockProtocol.messages = []string{} - - err := tt.sendFunc() - if err != nil { - t.Fatalf("Send function failed: %v", err) - } - - messages := sender.mockProtocol.GetMessages() - if len(messages) != 1 { - t.Fatalf("Expected 1 message, got %d", len(messages)) - } - - // Extract and validate message content - msg := messages[0] - contentStart := "Content: " - contentIdx := indexOf(msg, contentStart) - if contentIdx == -1 { - t.Fatalf("Could not find content in message: %s", msg) - } - - content := msg[contentIdx+len(contentStart):] - - var standardizedMsg types.StandardizedMessage - err = json.Unmarshal([]byte(content), &standardizedMsg) - if err != nil { - t.Fatalf("Failed to parse standardized message: %v", err) - } - - if standardizedMsg.ContentType != tt.expectedType { - t.Errorf("Expected type %s, got %s", tt.expectedType, standardizedMsg.ContentType) - } - }) - } -} - -func TestMessageOrderAndConsistency(t *testing.T) { - sender := NewTaskMessageSenderTest("test", "room") - agent := &IntegrationTestAgent{} - ctx := context.Background() - - // Run multiple times to test consistency - for i := 0; i < 3; i++ { - // Clear previous messages - sender.mockProtocol.messages = []string{} - - err := agent.ProcessTaskWithStreaming(ctx, fmt.Sprintf("test-%d", i), "room", sender) - if err != nil { - t.Fatalf("Run %d failed: %v", i, err) - } - - messages := sender.mockProtocol.GetMessages() - if len(messages) != 5 { - t.Errorf("Run %d: expected 5 messages, got %d", i, len(messages)) - } - - // Verify message order by checking types - expectedTypes := []string{"STRING", "JSON", "MD", "ARRAY", "STRING"} - for j, expectedType := range expectedTypes { - if j >= len(messages) { - continue - } - - msg := messages[j] - contentStart := "Content: " - contentIdx := indexOf(msg, contentStart) - if contentIdx == -1 { - continue - } - - content := msg[contentIdx+len(contentStart):] - var standardizedMsg types.StandardizedMessage - if err := json.Unmarshal([]byte(content), &standardizedMsg); err == nil { - if standardizedMsg.ContentType != expectedType { - t.Errorf("Run %d, Message %d: expected type %s, got %s", i, j+1, expectedType, standardizedMsg.ContentType) - } - } - } - } -} - -// helper functions -func contains(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} - -func indexOf(s, substr string) int { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return i - } - } - return -1 -} diff --git a/tests/integration/go.mod b/tests/integration/go.mod deleted file mode 100644 index 2355a3d..0000000 --- a/tests/integration/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module github.com/TeneoProtocolAI/teneo-agent-sdk/tests/integration - -go 1.21 - -require github.com/TeneoProtocolAI/teneo-agent-sdk v0.0.0 - -replace github.com/TeneoProtocolAI/teneo-agent-sdk => ../../ diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh deleted file mode 100755 index 09c1cf5..0000000 --- a/tests/run_all_tests.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/bin/bash - -# Teneo Agent SDK - Test Runner -# This script runs all tests in the organized test structure - -echo "🧪 Teneo Agent SDK Test Suite" -echo "============================" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Track test results -TOTAL_TESTS=0 -PASSED_TESTS=0 -FAILED_TESTS=0 - -# Function to run tests in a directory -run_test_suite() { - local test_dir=$1 - local test_name=$2 - - echo -e "\n${BLUE}šŸ“‹ Running $test_name Tests${NC}" - echo "----------------------------------------" - - if [ -d "$test_dir" ]; then - cd "$test_dir" - - # Check if go.mod exists - if [ -f "go.mod" ]; then - # Run tests with verbose output - if go test -v ./...; then - echo -e "${GREEN}āœ… $test_name tests passed${NC}" - PASSED_TESTS=$((PASSED_TESTS + 1)) - else - echo -e "${RED}āŒ $test_name tests failed${NC}" - FAILED_TESTS=$((FAILED_TESTS + 1)) - fi - else - echo -e "${YELLOW}āš ļø No go.mod found in $test_dir${NC}" - fi - - cd - > /dev/null - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - else - echo -e "${YELLOW}āš ļø Test directory $test_dir not found${NC}" - fi -} - -# Get the script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -# Run SDK core tests -echo -e "\n${BLUE}šŸ”§ Running SDK Core Tests${NC}" -echo "----------------------------------------" -cd .. -if go test -v ./pkg/...; then - echo -e "${GREEN}āœ… SDK core tests passed${NC}" - PASSED_TESTS=$((PASSED_TESTS + 1)) -else - echo -e "${RED}āŒ SDK core tests failed${NC}" - FAILED_TESTS=$((FAILED_TESTS + 1)) -fi -TOTAL_TESTS=$((TOTAL_TESTS + 1)) -cd "$SCRIPT_DIR" - -# Run organized test suites -run_test_suite "unit" "Unit" -run_test_suite "integration" "Integration" - -# Check if e2e tests exist -if [ -d "e2e" ] && [ "$(ls -A e2e)" ]; then - run_test_suite "e2e" "End-to-End" -else - echo -e "\n${YELLOW}āš ļø No E2E tests found (this is normal for now)${NC}" -fi - -# Run example tests -echo -e "\n${BLUE}šŸ“‹ Running Example Tests${NC}" -echo "----------------------------------------" -cd ../examples - -# Test standardized messaging example -if [ -d "standardized-messaging" ]; then - echo "Testing standardized-messaging example..." - cd standardized-messaging - if go run main.go > /dev/null 2>&1; then - echo -e "${GREEN}āœ… Standardized messaging example works${NC}" - PASSED_TESTS=$((PASSED_TESTS + 1)) - else - echo -e "${RED}āŒ Standardized messaging example failed${NC}" - FAILED_TESTS=$((FAILED_TESTS + 1)) - fi - cd .. - TOTAL_TESTS=$((TOTAL_TESTS + 1)) -fi - -# Test agent naming example -if [ -d "agent-naming" ]; then - echo "Testing agent-naming example..." - cd agent-naming - if timeout 10s go run main.go > /dev/null 2>&1; then - echo -e "${GREEN}āœ… Agent naming example works${NC}" - PASSED_TESTS=$((PASSED_TESTS + 1)) - else - echo -e "${RED}āŒ Agent naming example failed${NC}" - FAILED_TESTS=$((FAILED_TESTS + 1)) - fi - cd .. - TOTAL_TESTS=$((TOTAL_TESTS + 1)) -fi - -cd "$SCRIPT_DIR" - -# Display final results -echo -e "\n${BLUE}šŸ“Š Test Results Summary${NC}" -echo "========================" -echo -e "Total Test Suites: $TOTAL_TESTS" -echo -e "${GREEN}Passed: $PASSED_TESTS${NC}" -echo -e "${RED}Failed: $FAILED_TESTS${NC}" - -if [ $FAILED_TESTS -eq 0 ]; then - echo -e "\n${GREEN}šŸŽ‰ All tests passed!${NC}" - exit 0 -else - echo -e "\n${RED}šŸ’„ Some tests failed!${NC}" - exit 1 -fi diff --git a/tests/unit/agent_naming_test.go b/tests/unit/agent_naming_test.go deleted file mode 100644 index ec74c43..0000000 --- a/tests/unit/agent_naming_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package unit - -import ( - "testing" - - "github.com/TeneoProtocolAI/teneo-agent-sdk/pkg/naming" - "github.com/TeneoProtocolAI/teneo-agent-sdk/pkg/types" -) - -func TestAgentNamingConventions(t *testing.T) { - t.Run("DefaultValidator", func(t *testing.T) { - validator := naming.NewDefaultValidator() - - // Test valid names - validNames := []string{ - "security-scanner", - "data-processor-v2", - "my-agent", - "blockchain-validator", - } - - for _, name := range validNames { - result := validator.ValidateName(name) - if !result.IsValid { - t.Errorf("Expected '%s' to be valid, but got errors: %v", name, result.Errors) - } - } - - // Test invalid names - invalidNames := []string{ - "a", // too short - "123invalid", // starts with number - "system", // reserved - "agent@name!", // invalid characters - } - - for _, name := range invalidNames { - result := validator.ValidateName(name) - if result.IsValid { - t.Errorf("Expected '%s' to be invalid, but validation passed", name) - } - } - }) - - t.Run("StrictValidator", func(t *testing.T) { - validator := naming.NewStrictValidator() - - // Test valid strict names - validNames := []string{ - "security-scanner-agent", - "data-processor-agent", - } - - for _, name := range validNames { - result := validator.ValidateName(name) - if !result.IsValid { - t.Errorf("Expected '%s' to be valid with strict rules, but got errors: %v", name, result.Errors) - } - } - - // Test invalid strict names - invalidNames := []string{ - "SecurityAgent", // uppercase - "security_agent", // underscores - "security-scanner", // missing -agent suffix - } - - for _, name := range invalidNames { - result := validator.ValidateName(name) - if result.IsValid { - t.Errorf("Expected '%s' to be invalid with strict rules, but validation passed", name) - } - } - }) -} - -func TestNameNormalization(t *testing.T) { - validator := naming.NewDefaultValidator() - - tests := []struct { - input string - expected string - }{ - {"Security Scanner", "securityscanner"}, - {"DATA_PROCESSOR", "data_processor"}, - {"agent@name!", "agentname"}, - {"MyAwesome-Agent", "myawesome-agent"}, - } - - for _, tt := range tests { - result := validator.NormalizeName(tt.input) - if result != tt.expected { - t.Errorf("NormalizeName('%s') = '%s', expected '%s'", tt.input, result, tt.expected) - } - } -} - -func TestNameGeneration(t *testing.T) { - validator := naming.NewDefaultValidator() - - tests := []struct { - baseName string - purpose string - contains []string - }{ - {"security", "scanner", []string{"security", "scanner"}}, - {"data", "analyzer", []string{"data", "analyzer"}}, - {"", "scanner", []string{"scanner"}}, - {"auth", "", []string{"auth"}}, - {"", "", []string{"custom"}}, - } - - for _, tt := range tests { - result := validator.GenerateName(tt.baseName, tt.purpose) - - // Validate the generated name - validation := validator.ValidateName(result) - if !validation.IsValid { - t.Errorf("Generated name '%s' is not valid: %v", result, validation.Errors) - } - - // Check if it contains expected parts - for _, expected := range tt.contains { - if !contains(result, expected) { - t.Errorf("Generated name '%s' should contain '%s'", result, expected) - } - } - } -} - -func TestSuggestions(t *testing.T) { - validator := naming.NewDefaultValidator() - - invalidNames := []string{ - "123InvalidAgent", - "agent@name!", - "a", - } - - for _, invalidName := range invalidNames { - suggestions := validator.SuggestNames(invalidName, 3) - - if len(suggestions) == 0 { - t.Errorf("Expected suggestions for '%s', but got none", invalidName) - continue - } - - // Validate that suggestions are actually valid - for _, suggestion := range suggestions { - result := validator.ValidateName(suggestion) - if !result.IsValid { - t.Errorf("Suggestion '%s' for '%s' is not valid: %v", suggestion, invalidName, result.Errors) - } - } - } -} - -func TestAgentConfigIntegration(t *testing.T) { - validator := naming.NewDefaultValidator() - - // Valid config - validConfig := &types.AgentConfig{ - Name: "security-scanner-v2", - NamingRules: &types.AgentNamingRules{ - MaxLength: 30, - MinLength: 5, - CaseSensitive: false, - AllowNumbers: true, - AllowHyphens: true, - AllowUnderscores: false, - }, - } - - result := validator.ValidateAgentConfig(validConfig) - if !result.IsValid { - t.Errorf("Expected valid config to pass validation, but got errors: %v", result.Errors) - } - - // Invalid config - invalidConfig := &types.AgentConfig{ - Name: "InvalidName123!", - NamingRules: &types.AgentNamingRules{ - MaxLength: 20, - MinLength: 8, - CaseSensitive: true, - AllowNumbers: false, - AllowHyphens: true, - AllowUnderscores: false, - RequiredSuffix: "-agent", - }, - } - - result = validator.ValidateAgentConfig(invalidConfig) - if result.IsValid { - t.Error("Expected invalid config to fail validation") - } -} - -func TestReservedNames(t *testing.T) { - validator := naming.NewDefaultValidator() - - reservedNames := []string{ - "system", "admin", "root", "coordinator", - "teneo", "protocol", "network", - "api", "gateway", "proxy", - "agent", "bot", "service", - "test", "demo", "example", - } - - for _, name := range reservedNames { - result := validator.ValidateName(name) - if result.IsValid { - t.Errorf("Reserved name '%s' should not be valid", name) - } - } -} - -func TestCustomRules(t *testing.T) { - // Use the built-in strict validator instead of creating custom rules manually - validator := naming.NewStrictValidator() - - // Should be valid with strict rules - validName := "custom-security-agent" - result := validator.ValidateName(validName) - if !result.IsValid { - t.Errorf("Expected '%s' to be valid with strict rules, but got errors: %v", validName, result.Errors) - } - - // Should be invalid - missing -agent suffix - invalidName := "security-scanner" - result = validator.ValidateName(invalidName) - if result.IsValid { - t.Errorf("Expected '%s' to be invalid without required suffix", invalidName) - } -} - -// helper function -func contains(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/tests/unit/go.mod b/tests/unit/go.mod deleted file mode 100644 index 7691724..0000000 --- a/tests/unit/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module github.com/TeneoProtocolAI/teneo-agent-sdk/tests/unit - -go 1.21 - -require github.com/TeneoProtocolAI/teneo-agent-sdk v0.0.0 - -replace github.com/TeneoProtocolAI/teneo-agent-sdk => ../../ diff --git a/tests/unit/standardized_messaging_test.go b/tests/unit/standardized_messaging_test.go deleted file mode 100644 index 57b79b9..0000000 --- a/tests/unit/standardized_messaging_test.go +++ /dev/null @@ -1,360 +0,0 @@ -package unit - -import ( - "context" - "encoding/json" - "fmt" - "log" - "testing" - "time" - - "github.com/TeneoProtocolAI/teneo-agent-sdk/pkg/types" -) - -// TestMessageSender implements MessageSender for testing -type TestMessageSender struct { - messages []string - taskID string - room string -} - -// NewTestMessageSender creates a test message sender -func NewTestMessageSender(taskID, room string) *TestMessageSender { - return &TestMessageSender{ - messages: make([]string, 0), - taskID: taskID, - room: room, - } -} - -// SendMessage implements backward compatibility (STRING type) -func (t *TestMessageSender) SendMessage(content string) error { - return t.sendStandardizedMessage(types.StandardMessageTypeString, content) -} - -// SendTaskUpdate implements task updates -func (t *TestMessageSender) SendTaskUpdate(content string) error { - updateContent := fmt.Sprintf("šŸ”„ Update: %s", content) - return t.sendStandardizedMessage(types.StandardMessageTypeString, updateContent) -} - -// SendMessageAsJSON implements JSON message sending -func (t *TestMessageSender) SendMessageAsJSON(content interface{}) error { - return t.sendStandardizedMessage(types.StandardMessageTypeJSON, content) -} - -// SendMessageAsMD implements markdown message sending -func (t *TestMessageSender) SendMessageAsMD(content string) error { - return t.sendStandardizedMessage(types.StandardMessageTypeMD, content) -} - -// SendMessageAsArray implements array message sending -func (t *TestMessageSender) SendMessageAsArray(content []interface{}) error { - return t.sendStandardizedMessage(types.StandardMessageTypeArray, content) -} - -// sendStandardizedMessage handles the core standardized message logic -func (t *TestMessageSender) sendStandardizedMessage(msgType string, content interface{}) error { - standardizedMsg := types.StandardizedMessage{ - ContentType: msgType, - Content: content, - } - - // marshal the standardized message - contentJSON, err := json.Marshal(standardizedMsg) - if err != nil { - return fmt.Errorf("failed to marshal standardized message: %w", err) - } - - // simulate sending (store for testing) - message := fmt.Sprintf("[%s:%s] %s", t.taskID, t.room, string(contentJSON)) - t.messages = append(t.messages, message) - - return nil -} - -// GetMessages returns all sent messages -func (t *TestMessageSender) GetMessages() []string { - return t.messages -} - -// TestAgent implements StreamingTaskHandler for testing -type TestAgent struct{} - -// ProcessTask implements basic agent interface -func (a *TestAgent) ProcessTask(ctx context.Context, task string) (string, error) { - return "Basic task completed", nil -} - -// ProcessTaskWithStreaming demonstrates standardized message usage -func (a *TestAgent) ProcessTaskWithStreaming(ctx context.Context, task, room string, sender types.MessageSender) error { - // Test 1: String message (backward compatibility) - if err := sender.SendMessage("Testing standardized message functions"); err != nil { - return fmt.Errorf("string message test failed: %w", err) - } - - // Test 2: JSON message - jsonData := map[string]interface{}{ - "test_suite": "standardized_messaging", - "timestamp": time.Now().Format(time.RFC3339), - "status": "running", - "results": map[string]interface{}{ - "total_tests": 4, - "passed": 0, - }, - } - - if err := sender.SendMessageAsJSON(jsonData); err != nil { - return fmt.Errorf("JSON message test failed: %w", err) - } - - // Test 3: Markdown message - markdownContent := `# Test Report - -## Results -- āœ… **String Messages**: Working -- āœ… **JSON Messages**: Working -- ā³ **Markdown Messages**: Testing... - -### Next -Array message testing.` - - if err := sender.SendMessageAsMD(markdownContent); err != nil { - return fmt.Errorf("markdown message test failed: %w", err) - } - - // Test 4: Array message - arrayData := []interface{}{ - map[string]interface{}{ - "test": "string_message", - "status": "passed", - }, - map[string]interface{}{ - "test": "json_message", - "status": "passed", - }, - map[string]interface{}{ - "test": "markdown_message", - "status": "passed", - }, - "simple_string_item", - 42, - true, - } - - if err := sender.SendMessageAsArray(arrayData); err != nil { - return fmt.Errorf("array message test failed: %w", err) - } - - // Final message - if err := sender.SendMessage("āœ… All tests completed successfully!"); err != nil { - return fmt.Errorf("final message test failed: %w", err) - } - - return nil -} - -func TestStandardizedMessageFunctions(t *testing.T) { - log.Println("šŸš€ Testing Standardized Message Functions") - - // Create test components - taskID := "test-001" - room := "test-room" - testSender := NewTestMessageSender(taskID, room) - testAgent := &TestAgent{} - - // Run test - ctx := context.Background() - task := "test task" - - err := testAgent.ProcessTaskWithStreaming(ctx, task, room, testSender) - if err != nil { - t.Fatalf("Test failed: %v", err) - } - - // Validate results - messages := testSender.GetMessages() - if len(messages) != 5 { - t.Errorf("Expected 5 messages, got %d", len(messages)) - } - - // Validate message types - expectedTypes := []string{"STRING", "JSON", "MD", "ARRAY", "STRING"} - for i, expectedType := range expectedTypes { - if i < len(messages) { - msg := messages[i] - jsonStart := fmt.Sprintf("[%s:%s] ", taskID, room) - if len(msg) > len(jsonStart) { - jsonPart := msg[len(jsonStart):] - var standardizedMsg types.StandardizedMessage - if err := json.Unmarshal([]byte(jsonPart), &standardizedMsg); err == nil { - if standardizedMsg.ContentType != expectedType { - t.Errorf("Message %d: expected type %s, got %s", i+1, expectedType, standardizedMsg.ContentType) - } - } else { - t.Errorf("Message %d: parse error: %v", i+1, err) - } - } - } - } -} - -func TestMessageSenderStringMessages(t *testing.T) { - sender := NewTestMessageSender("test", "room") - - err := sender.SendMessage("Test message") - if err != nil { - t.Fatalf("SendMessage failed: %v", err) - } - - messages := sender.GetMessages() - if len(messages) != 1 { - t.Fatalf("Expected 1 message, got %d", len(messages)) - } - - // Parse and validate - jsonPart := messages[0][len("[test:room] "):] - var msg types.StandardizedMessage - err = json.Unmarshal([]byte(jsonPart), &msg) - if err != nil { - t.Fatalf("Failed to parse message: %v", err) - } - - if msg.ContentType != types.StandardMessageTypeString { - t.Errorf("Expected STRING type, got %s", msg.ContentType) - } - - if msg.Content != "Test message" { - t.Errorf("Expected 'Test message', got %v", msg.Content) - } -} - -func TestMessageSenderJSONMessages(t *testing.T) { - sender := NewTestMessageSender("test", "room") - - testData := map[string]interface{}{ - "key1": "value1", - "key2": 42, - "key3": true, - "nested": map[string]interface{}{ - "inner": "value", - }, - } - - err := sender.SendMessageAsJSON(testData) - if err != nil { - t.Fatalf("SendMessageAsJSON failed: %v", err) - } - - messages := sender.GetMessages() - if len(messages) != 1 { - t.Fatalf("Expected 1 message, got %d", len(messages)) - } - - // Parse and validate - jsonPart := messages[0][len("[test:room] "):] - var msg types.StandardizedMessage - err = json.Unmarshal([]byte(jsonPart), &msg) - if err != nil { - t.Fatalf("Failed to parse message: %v", err) - } - - if msg.ContentType != types.StandardMessageTypeJSON { - t.Errorf("Expected JSON type, got %s", msg.ContentType) - } - - // Validate content structure - contentMap, ok := msg.Content.(map[string]interface{}) - if !ok { - t.Fatalf("Expected content to be map[string]interface{}, got %T", msg.Content) - } - - if contentMap["key1"] != "value1" { - t.Errorf("Expected key1='value1', got %v", contentMap["key1"]) - } -} - -func TestMessageSenderMarkdownMessages(t *testing.T) { - sender := NewTestMessageSender("test", "room") - - markdown := `# Test Header - -This is **bold** and *italic* text. - -## List -- Item 1 -- Item 2` - - err := sender.SendMessageAsMD(markdown) - if err != nil { - t.Fatalf("SendMessageAsMD failed: %v", err) - } - - messages := sender.GetMessages() - if len(messages) != 1 { - t.Fatalf("Expected 1 message, got %d", len(messages)) - } - - // Parse and validate - jsonPart := messages[0][len("[test:room] "):] - var msg types.StandardizedMessage - err = json.Unmarshal([]byte(jsonPart), &msg) - if err != nil { - t.Fatalf("Failed to parse message: %v", err) - } - - if msg.ContentType != types.StandardMessageTypeMD { - t.Errorf("Expected MD type, got %s", msg.ContentType) - } - - if msg.Content != markdown { - t.Errorf("Markdown content mismatch") - } -} - -func TestMessageSenderArrayMessages(t *testing.T) { - sender := NewTestMessageSender("test", "room") - - arrayData := []interface{}{ - "string item", - 42, - true, - map[string]interface{}{"key": "value"}, - []string{"nested", "array"}, - } - - err := sender.SendMessageAsArray(arrayData) - if err != nil { - t.Fatalf("SendMessageAsArray failed: %v", err) - } - - messages := sender.GetMessages() - if len(messages) != 1 { - t.Fatalf("Expected 1 message, got %d", len(messages)) - } - - // Parse and validate - jsonPart := messages[0][len("[test:room] "):] - var msg types.StandardizedMessage - err = json.Unmarshal([]byte(jsonPart), &msg) - if err != nil { - t.Fatalf("Failed to parse message: %v", err) - } - - if msg.ContentType != types.StandardMessageTypeArray { - t.Errorf("Expected ARRAY type, got %s", msg.ContentType) - } - - contentArray, ok := msg.Content.([]interface{}) - if !ok { - t.Fatalf("Expected content to be []interface{}, got %T", msg.Content) - } - - if len(contentArray) != 5 { - t.Errorf("Expected 5 array items, got %d", len(contentArray)) - } - - if contentArray[0] != "string item" { - t.Errorf("Expected first item to be 'string item', got %v", contentArray[0]) - } -} From 0b7a01ee999602bfefdcf995b34cbaeab995102e Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:44:21 +0700 Subject: [PATCH 5/9] Delete .github/pull_request_template.md --- .github/pull_request_template.md | 46 -------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index a1dee16..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,46 +0,0 @@ -## Description - - - -## Type of Change - -- [ ] Bug fix -- [ ] New feature -- [ ] Documentation update -- [ ] Performance improvement -- [ ] Code refactoring -- [ ] Other (please describe): - -## Changes Made - - - -- -- -- - -## Testing - - - -- [ ] Tested locally -- [ ] Added/updated tests -- [ ] All tests pass - -## Screenshots (if applicable) - - - -## Additional Notes - - - ---- - -### Checklist - -- [ ] My code follows the project's coding style -- [ ] I have tested my changes -- [ ] I have updated the documentation (if needed) -- [ ] My changes don't introduce new warnings -- [ ] I have added comments for complex code (if needed) From 7a83fe0e0b7617332e72a2e0b427539f9280823e Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:44:41 +0700 Subject: [PATCH 6/9] Delete .github/workflows directory --- .github/workflows/claude-code-review.yml | 82 ------------------ .github/workflows/claude.yml | 65 --------------- .github/workflows/mirroring.yml | 24 ------ .github/workflows/top-issue.yml | 102 ----------------------- 4 files changed, 273 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml delete mode 100644 .github/workflows/claude.yml delete mode 100644 .github/workflows/mirroring.yml delete mode 100644 .github/workflows/top-issue.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index e34b9a0..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Only run if the repository is in the Teneo-Protocol organization - if: github.repository_owner == 'Teneo-Protocol' - - # Optional: Filter by PR author - # if: | - # github.repository_owner == 'Teneo-Protocol' && - # (github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR') - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) - # model: "claude-opus-4-1-20250805" - - # Direct prompt for automated review (no @claude mention needed) - direct_prompt: | - Please review this pull request and provide feedback on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security concerns - - Test coverage - - Be constructive and helpful in your feedback. - - # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR - # use_sticky_comment: true - - # Optional: Customize review based on file types - # direct_prompt: | - # Review this PR focusing on: - # - For TypeScript files: Type safety and proper interface usage - # - For API endpoints: Security, input validation, and error handling - # - For React components: Performance, accessibility, and best practices - # - For tests: Coverage, edge cases, and test quality - - # Optional: Different prompts for different authors - # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && - # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || - # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - - # Optional: Add specific tools for running tests or linting - # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - - # Optional: Skip review for certain conditions - # if: | - # !contains(github.event.pull_request.title, '[skip-review]') && - # !contains(github.event.pull_request.title, '[WIP]') - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 6f34b5e..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - github.repository_owner == 'Teneo-Protocol' && - ((github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) - # model: "claude-opus-4-1-20250805" - - # Optional: Customize the trigger phrase (default: @claude) - # trigger_phrase: "/claude" - - # Optional: Trigger when specific user is assigned to an issue - # assignee_trigger: "claude-bot" - - # Optional: Allow Claude to run specific commands - # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - - # Optional: Add custom instructions for Claude to customize its behavior for your project - # custom_instructions: | - # Follow our coding standards - # Ensure all new code has tests - # Use TypeScript for new files - - # Optional: Custom environment variables for Claude - # claude_env: | - # NODE_ENV: test - diff --git a/.github/workflows/mirroring.yml b/.github/workflows/mirroring.yml deleted file mode 100644 index bfc8462..0000000 --- a/.github/workflows/mirroring.yml +++ /dev/null @@ -1,24 +0,0 @@ - -name: Mirror Release Branch -on: - workflow_dispatch: - -jobs: - mirror-to-test-repo: - # Only run if the repository is in the Teneo-Protocol organization - if: github.repository_owner == 'Teneo-Protocol' - runs-on: ubuntu-latest - steps: - - name: Checkout Source Repo - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Mirror main → main - uses: yesolutions/mirror-action@master - with: - REMOTE: git@github.com:TeneoProtocolAI/teneo-agent-sdk.git - GIT_SSH_PRIVATE_KEY: ${{ secrets.GIT_SSH_PRIVATE_KEY }} - PUSH_ALL_REFS: "false" - GIT_PUSH_ARGS: "--tags --force --prune" - GIT_SSH_NO_VERIFY_HOST: "true" diff --git a/.github/workflows/top-issue.yml b/.github/workflows/top-issue.yml deleted file mode 100644 index ed7bdb1..0000000 --- a/.github/workflows/top-issue.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Top Issues by Votes - -on: - schedule: - - cron: "0 */2 * * *" # Run every 2 hours - workflow_dispatch: # Allow manual triggering - -permissions: - issues: write - contents: read - -jobs: - update-top-issues: - # Only run if the repository is in the Teneo-Protocol organization - if: github.repository_owner == 'TeneoProtocolAI' - runs-on: ubuntu-latest - - steps: - - name: Update top issues list for teneo-agent-sdk - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Set this to the issue number where you want to track top issues - # Leave empty to skip updating (will only log) - ISSUE_NUMBER: "4" - run: | - #!/bin/bash - set -e - - echo "Fetching all open issues for teneo-agent-sdk..." - - # Extract owner and repo from GitHub context - REPO_OWNER="${{ github.repository_owner }}" - REPO_NAME="${{ github.event.repository.name }}" - - echo "Repository: $REPO_OWNER/$REPO_NAME" - - # Fetch all open issues with reaction data - issues=$(gh api graphql -f query=' - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - issues(first: 100, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) { - nodes { - number - title - url - reactions(content: THUMBS_UP) { - totalCount - } - } - } - } - } - ' -f owner="$REPO_OWNER" -f repo="$REPO_NAME") - - # Parse and sort issues by thumbs up count - sorted_issues=$(echo "$issues" | jq -r --arg ISSUE_NUMBER "$ISSUE_NUMBER" ' - .data.repository.issues.nodes - | map(select(.number != ($ISSUE_NUMBER | tonumber))) - | sort_by(-.reactions.totalCount) - | to_entries - | map("\(.key + 1). [\(.value.title)](\(.value.url)) - \(.value.reactions.totalCount) šŸ‘") - | join("\n") - ') - - # Create issue body - current_time=$(date -u +"%Y-%m-%d %H:%M:%S UTC") - - { - echo "# šŸ‘ Top Issues by Community Votes" - echo "" - echo "This list shows the most requested features and reported bugs for **teneo-agent-sdk**," - echo "automatically updated every 6 hours based on šŸ‘ reactions." - echo "" - echo "**Last updated:** $current_time" - echo "" - echo "## Top Issues" - echo "" - if [ -n "$sorted_issues" ]; then - echo "$sorted_issues" - else - echo "No issues found or all issues have 0 votes." - fi - echo "" - echo "---" - echo "šŸ’” *Vote on issues by adding a šŸ‘ reaction to help prioritize development*" - echo "" - echo "šŸ¤– *Automatically updated by [GitHub Actions](.github/workflows/top-issue.yml)*" - } > /tmp/issue_body.md - - # Update the tracking issue if ISSUE_NUMBER is set - if [ -n "$ISSUE_NUMBER" ]; then - echo "Updating tracking issue #$ISSUE_NUMBER..." - gh issue edit "$ISSUE_NUMBER" \ - --repo "$REPO_OWNER/$REPO_NAME" \ - --body-file /tmp/issue_body.md - echo "āœ… Successfully updated top issues list!" - else - echo "šŸ“‹ Top issues list (ISSUE_NUMBER not set, skipping update):" - cat /tmp/issue_body.md - echo "" - echo "šŸ’” To enable automatic tracking, create an issue and set ISSUE_NUMBER in the workflow." - fi \ No newline at end of file From 82c2dca54835850d091d0853c2141ec5eb510745 Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:47:24 +0700 Subject: [PATCH 7/9] Create .keep --- modules/.keep | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules/.keep diff --git a/modules/.keep b/modules/.keep new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/modules/.keep @@ -0,0 +1 @@ +placeholder From 4d7d507cf00f028f8fd832de1fa2d7a1527612c5 Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:48:34 +0700 Subject: [PATCH 8/9] Add files via upload --- modules/coingecko.go | 159 +++++++++++++++++++++++++ modules/common.go | 8 ++ modules/compat_coingecko.go | 223 ++++++++++++++++++++++++++++++++++++ modules/detection.go | 30 +++++ modules/gpt.go | 215 ++++++++++++++++++++++++++++++++++ modules/gpt.go.bak | 152 ++++++++++++++++++++++++ modules/hype.go | 19 +++ modules/market_handlers.go | 157 +++++++++++++++++++++++++ modules/risk.go | 19 +++ modules/scan.go | 24 ++++ modules/sentiment.go | 19 +++ modules/summary.go | 5 + modules/topcalls.go | 12 ++ modules/utils.go | 44 +++++++ modules/watch.go | 11 ++ modules/xscanner.go | 72 ++++++++++++ 16 files changed, 1169 insertions(+) create mode 100644 modules/coingecko.go create mode 100644 modules/common.go create mode 100644 modules/compat_coingecko.go create mode 100644 modules/detection.go create mode 100644 modules/gpt.go create mode 100644 modules/gpt.go.bak create mode 100644 modules/hype.go create mode 100644 modules/market_handlers.go create mode 100644 modules/risk.go create mode 100644 modules/scan.go create mode 100644 modules/sentiment.go create mode 100644 modules/summary.go create mode 100644 modules/topcalls.go create mode 100644 modules/utils.go create mode 100644 modules/watch.go create mode 100644 modules/xscanner.go diff --git a/modules/coingecko.go b/modules/coingecko.go new file mode 100644 index 0000000..54e5bf9 --- /dev/null +++ b/modules/coingecko.go @@ -0,0 +1,159 @@ +package modules + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + "time" +) + +// Small coin symbol -> coingecko id mapping for common tokens. +// Keys must be unique and lowercase. +var cgSymbolToID = map[string]string{ + "btc": "bitcoin", + "eth": "ethereum", + "bnb": "binancecoin", + "sol": "solana", + "matic": "matic-network", + "ada": "cardano", + "doge": "dogecoin", + "usdt": "tether", + "usdc": "usd-coin", + "ltc": "litecoin", + "avax": "avalanche-2", + "dot": "polkadot", + "link": "chainlink", + "shib": "shiba-inu", + "uni": "uniswap", + "ftm": "fantom", + "atom": "cosmos", + "op": "optimism", + "arb": "arbitrum", +} + +// MarketData holds the values we extract from CoinGecko +type MarketData struct { + ID string + Symbol string + PriceUSD float64 + Change24h float64 // percentage + Volume24h float64 // in USD + MarketCapUSD float64 + RetrievedAt time.Time +} + +// cache entry +type cgCacheEntry struct { + data MarketData + expiresAt time.Time +} + +var ( + cgCache = map[string]cgCacheEntry{} + cgCacheMu = sync.Mutex{} + cacheTTL = 30 * time.Second + httpClient = &http.Client{Timeout: 10 * time.Second} +) + +// GetMarketData fetches market data for a symbol (e.g., "SOL", "BTC"). +func GetMarketData(symbol string) (MarketData, error) { + sym := strings.ToLower(strings.TrimSpace(symbol)) + // cache check + cgCacheMu.Lock() + if e, ok := cgCache[sym]; ok && time.Now().Before(e.expiresAt) { + cgCacheMu.Unlock() + return e.data, nil + } + cgCacheMu.Unlock() + + id, ok := cgSymbolToID[sym] + if !ok { + // try direct id fallback + id = sym + } + + url := fmt.Sprintf("https://api.coingecko.com/api/v3/coins/%s?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=false", id) + + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Accept", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + return MarketData{}, fmt.Errorf("coingecko http err: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return MarketData{}, fmt.Errorf("coingecko status %d", resp.StatusCode) + } + + var body map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return MarketData{}, fmt.Errorf("coingecko decode err: %w", err) + } + + md := MarketData{ + ID: id, + Symbol: sym, + RetrievedAt: time.Now(), + } + + if marketData, ok := body["market_data"].(map[string]interface{}); ok { + if cp, ok := marketData["current_price"].(map[string]interface{}); ok { + if usd, ok := cp["usd"].(float64); ok { + md.PriceUSD = usd + } + } + if ch, ok := marketData["price_change_percentage_24h"].(float64); ok { + md.Change24h = ch + } + if vol, ok := marketData["total_volume"].(map[string]interface{}); ok { + if v, ok := vol["usd"].(float64); ok { + md.Volume24h = v + } + } + if mc, ok := marketData["market_cap"].(map[string]interface{}); ok { + if m, ok := mc["usd"].(float64); ok { + md.MarketCapUSD = m + } + } + } + + // save to cache + cgCacheMu.Lock() + cgCache[sym] = cgCacheEntry{ + data: md, + expiresAt: time.Now().Add(cacheTTL), + } + cgCacheMu.Unlock() + + return md, nil +} + +// ComputeHypeScore builds a simple hype score [0..1] using change24h and volume/marketcap +func ComputeHypeScore(m MarketData) float64 { + score := 0.0 + clamp := func(v float64) float64 { + if v < 0 { + return 0 + } + if v > 1 { + return 1 + } + return v + } + score += clamp((m.Change24h+10)/40) * 0.6 + if m.MarketCapUSD > 0 { + r := (m.Volume24h / m.MarketCapUSD) + score += clamp(r*20) * 0.4 + } + if score < 0 { + score = 0 + } + if score > 1 { + score = 1 + } + return score +} diff --git a/modules/common.go b/modules/common.go new file mode 100644 index 0000000..6bfe9fe --- /dev/null +++ b/modules/common.go @@ -0,0 +1,8 @@ +package modules + +import "time" + +// Only keep TimeNowUTC here +func TimeNowUTC() time.Time { + return time.Now().UTC() +} diff --git a/modules/compat_coingecko.go b/modules/compat_coingecko.go new file mode 100644 index 0000000..318ca58 --- /dev/null +++ b/modules/compat_coingecko.go @@ -0,0 +1,223 @@ +package modules + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// Compatibility layer: provides the older helper names expected by main.go +// implemented on top of GetMarketData and direct CoinGecko queries. + +// GetCoinGeckoFull fetches the full CoinGecko JSON for a given id or symbol. +// Returns a generic map (same shape as JSON). +func GetCoinGeckoFull(idOrSymbol string) (map[string]interface{}, error) { + s := strings.TrimSpace(idOrSymbol) + if s == "" { + return nil, fmt.Errorf("empty idOrSymbol") + } + + // try symbol mapping first + l := strings.ToLower(s) + if id, ok := cgSymbolToID[l]; ok { + l = id + } + + url := fmt.Sprintf("https://api.coingecko.com/api/v3/coins/%s?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=false", l) + client := &http.Client{Timeout: 12 * time.Second} + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Accept", "application/json") + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("coingecko http err: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + // read body to include in error (but truncate) + bodyB, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return nil, fmt.Errorf("coingecko status %d: %s", resp.StatusCode, string(bodyB)) + } + + var out map[string]interface{} + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&out); err != nil { + return nil, fmt.Errorf("coingecko decode err: %w", err) + } + return out, nil +} + +// FormatCoinGeckoSummary formats a compact human readable summary from the +// full coin JSON (the shape returned by GetCoinGeckoFull). +func FormatCoinGeckoSummary(full map[string]interface{}) string { + if full == nil { + return "Coin data: unavailable" + } + + // helpers to extract safely + getString := func(keys ...string) string { + var cur interface{} = full + for _, k := range keys { + if m, ok := cur.(map[string]interface{}); ok { + cur = m[k] + } else { + return "" + } + } + if s, ok := cur.(string); ok { + return s + } + return "" + } + getFloat := func(keys ...string) float64 { + var cur interface{} = full + for _, k := range keys { + if m, ok := cur.(map[string]interface{}); ok { + cur = m[k] + } else { + return 0 + } + } + switch v := cur.(type) { + case float64: + return v + case int: + return float64(v) + case int64: + return float64(v) + default: + return 0 + } + } + + name := getString("name") + symbol := getString("symbol") + if symbol == "" { + // fallback try top-level id + symbol = getString("id") + } + price := getFloat("market_data", "current_price", "usd") + change24 := getFloat("market_data", "price_change_percentage_24h") + vol := getFloat("market_data", "total_volume", "usd") + mcap := getFloat("market_data", "market_cap", "usd") + + // Build summary + summary := fmt.Sprintf("%s (%s)\nPrice: $%.6f\n24h: %+0.2f%% • Volume: $%.0f • MarketCap: $%.0f", + nameOr(symbol, name), strings.ToUpper(symbol), price, change24, vol, mcap) + return summary +} + +func nameOr(sym, name string) string { + if name != "" { + return name + } + if sym != "" { + return strings.ToUpper(sym) + } + return "unknown" +} + +// GetMarketCap returns a human readable market cap string for the symbol. +// Signature matches main.go expectation: returns (string, error) +func GetMarketCap(symbol string) (string, error) { + if strings.TrimSpace(symbol) == "" { + return "Usage: marketcap [token]", nil + } + // Prefer using our fast GetMarketData cache + md, err := GetMarketData(symbol) + if err == nil { + if md.MarketCapUSD > 0 { + return fmt.Sprintf("$%.0f", md.MarketCapUSD), nil + } + // if not present, fall through to full fetch + } + + // fallback to full fetch + full, err := GetCoinGeckoFull(symbol) + if err != nil { + return "", err + } + mcap := safeGetFloat(full, "market_data", "market_cap", "usd") + if mcap <= 0 { + return "Market cap: unavailable", nil + } + return fmt.Sprintf("$%.0f", mcap), nil +} + +// GetVolume returns 24h volume for the symbol as string (string, error) +func GetVolume(symbol string) (string, error) { + if strings.TrimSpace(symbol) == "" { + return "Usage: volume [token]", nil + } + md, err := GetMarketData(symbol) + if err == nil { + if md.Volume24h > 0 { + return fmt.Sprintf("$%.0f", md.Volume24h), nil + } + } + full, err := GetCoinGeckoFull(symbol) + if err != nil { + return "", err + } + vol := safeGetFloat(full, "market_data", "total_volume", "usd") + if vol <= 0 { + return "Volume: unavailable", nil + } + return fmt.Sprintf("$%.0f", vol), nil +} + +// GetCoinPrice returns the current USD price as string (string, error) +func GetCoinPrice(symbol string) (string, error) { + if strings.TrimSpace(symbol) == "" { + return "Usage: price [token]", nil + } + md, err := GetMarketData(symbol) + if err == nil && md.PriceUSD > 0 { + return fmt.Sprintf("$%.6f", md.PriceUSD), nil + } + full, err := GetCoinGeckoFull(symbol) + if err != nil { + return "", err + } + price := safeGetFloat(full, "market_data", "current_price", "usd") + if price <= 0 { + return "Price: unavailable", nil + } + return fmt.Sprintf("$%.6f", price), nil +} + +// GetTrendSnapshot returns a short human-readable trend string for a token. +// Signature: (string, error) +func GetTrendSnapshot(symbol string) (string, error) { + if strings.TrimSpace(symbol) == "" { + return "Usage: trend [token]", nil + } + // Use quick market data + md, err := GetMarketData(symbol) + if err != nil { + // try full fallback for more fields + full, err2 := GetCoinGeckoFull(symbol) + if err2 != nil { + return "", err + } + // attempt to derive change + change := safeGetFloat(full, "market_data", "price_change_percentage_24h") + return fmt.Sprintf("Trend snapshot for %s: 24h change %.2f%%", strings.ToUpper(symbol), change), nil + } + // compute simple trend description + change := md.Change24h + trend := "neutral" + if change >= 5 { + trend = "strong bullish momentum" + } else if change >= 1 { + trend = "bullish momentum" + } else if change <= -5 { + trend = "sharp drop / bearish" + } else if change <= -1 { + trend = "bearish momentum" + } + return fmt.Sprintf("Trend snapshot for %s: %s (24h %+0.2f%%)", strings.ToUpper(symbol), trend, change), nil +} diff --git a/modules/detection.go b/modules/detection.go new file mode 100644 index 0000000..5e49c38 --- /dev/null +++ b/modules/detection.go @@ -0,0 +1,30 @@ +package modules + +import ( + "encoding/json" + "os" + "time" +) + +// Detection represents a single detection / signal from the scanner. +type Detection struct { + KOL string `json:"kol"` // nama KOL / influencer + Token string `json:"token"` // ticker / token id / nama + Signal string `json:"signal"` // e.g. "early_call", "dump_warning" + Confidence float64 `json:"confidence"` // 0..1 + Source string `json:"source"` // source layanan: "x", "coingecko", etc. + Text string `json:"text"` // raw text/snippet yang memicu deteksi + Link string `json:"link"` // optional link (tweet, post, tx) + Timestamp time.Time `json:"timestamp"` // waktu deteksi +} + +// SaveDetection writes a Detection as pretty JSON to filename (overwrites/creates). +// This is simple and safe for mock/stub usage. +func SaveDetection(filename string, d Detection) error { + b, err := json.MarshalIndent(d, "", " ") + if err != nil { + return err + } + // write file (overwrite). If you prefer append, change os.WriteFile to open/append style. + return os.WriteFile(filename, b, 0644) +} diff --git a/modules/gpt.go b/modules/gpt.go new file mode 100644 index 0000000..7c2ec6c --- /dev/null +++ b/modules/gpt.go @@ -0,0 +1,215 @@ +package modules + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "time" +) + +// ForwardToOpenAI sends prompt to Google Gemini (preferred) or OpenAI (fallback). +// Important fix: always normalize GOOGLE_MODEL by STRIPPING leading "models/" if present, +// then build endpoint: /v1beta/models/{modelName}:generateContent +func ForwardToOpenAI(prompt string) (string, error) { + prompt = strings.TrimSpace(prompt) + if prompt == "" { + return "", fmt.Errorf("empty prompt") + } + + googleKey := strings.TrimSpace(os.Getenv("GOOGLE_API_KEY")) + openaiKey := strings.TrimSpace(os.Getenv("OPENAI_API_KEY")) + + shortKey := func(k string) string { + if k == "" { + return "" + } + if len(k) <= 8 { + return k + } + return k[:8] + "..." + } + + // Prefer Google Gemini if key present + if googleKey != "" { + modelEnv := strings.TrimSpace(os.Getenv("GOOGLE_MODEL")) + if modelEnv == "" { + modelEnv = "gemini-2.5-flash" + } + + // **NORMALIZE**: strip any leading "models/" if present + if strings.HasPrefix(modelEnv, "models/") { + modelEnv = strings.TrimPrefix(modelEnv, "models/") + } + + // Now modelEnv is plain model name (e.g. "gemini-2.5-flash") + // Construct endpoint: /v1beta/models/{modelEnv}:generateContent + url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", modelEnv, googleKey) + + // Build request body per Gemini docs + reqBody := map[string]interface{}{ + "contents": []interface{}{ + map[string]interface{}{ + "parts": []interface{}{ + map[string]interface{}{"text": prompt}, + }, + }, + }, + "generationConfig": map[string]interface{}{ + "maxOutputTokens": 256, + "temperature": 0.2, + }, + } + b, _ := json.Marshal(reqBody) + + log.Printf("ForwardToOpenAI: Google request -> model=%s key_preview=%s prompt_len=%d", + modelEnv, shortKey(googleKey), len(prompt)) + + req, err := http.NewRequest("POST", url, bytes.NewReader(b)) + if err != nil { + return "", fmt.Errorf("failed build request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-goog-api-key", googleKey) + + client := &http.Client{Timeout: 25 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("google http err: %w", err) + } + defer resp.Body.Close() + respBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 200*1024)) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Printf("ForwardToOpenAI: Google response status=%d body_preview=%s", resp.StatusCode, sanitizeForLog(string(respBytes))) + return "", fmt.Errorf("google api error: status %d: %s", resp.StatusCode, sanitizeForLog(string(respBytes))) + } + + // parse response and extract candidate text + var parsed map[string]interface{} + if err := json.Unmarshal(respBytes, &parsed); err != nil { + log.Printf("ForwardToOpenAI: google parse json err: %v", err) + return strings.TrimSpace(string(respBytes)), nil + } + + // Typical path: candidates[0].content.parts[0].text + if cands, ok := parsed["candidates"].([]interface{}); ok && len(cands) > 0 { + if cand0, ok := cands[0].(map[string]interface{}); ok { + if content, ok := cand0["content"].(map[string]interface{}); ok { + if parts, ok := content["parts"].([]interface{}); ok && len(parts) > 0 { + if p0, ok := parts[0].(map[string]interface{}); ok { + if txt, ok := p0["text"].(string); ok && txt != "" { + return strings.TrimSpace(txt), nil + } + } + } + } + if txt, ok := cand0["text"].(string); ok && txt != "" { + return strings.TrimSpace(txt), nil + } + } + } + + // fallback path: output.content.parts[0].text + if out, ok := parsed["output"].(map[string]interface{}); ok { + if content, ok := out["content"].(map[string]interface{}); ok { + if parts, ok := content["parts"].([]interface{}); ok && len(parts) > 0 { + if p0, ok := parts[0].(map[string]interface{}); ok { + if txt, ok := p0["text"].(string); ok && txt != "" { + return strings.TrimSpace(txt), nil + } + } + } + } + } + + // final fallback: first string leaf + if s := findFirstString(parsed); s != "" { + return strings.TrimSpace(s), nil + } + return strings.TrimSpace(string(respBytes)), nil + } + + // fallback: OpenAI + if openaiKey != "" { + reqURL := "https://api.openai.com/v1/chat/completions" + reqBodyMap := map[string]interface{}{ + "model": "gpt-4o-mini", + "messages": []map[string]interface{}{ + {"role": "user", "content": prompt}, + }, + "max_tokens": 256, + "temperature": 0.2, + } + reqB, _ := json.Marshal(reqBodyMap) + req, _ := http.NewRequest("POST", reqURL, bytes.NewReader(reqB)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+openaiKey) + + client := &http.Client{Timeout: 20 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("openai http err: %w", err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(io.LimitReader(resp.Body, 200*1024)) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Printf("ForwardToOpenAI: OpenAI response status=%d body_preview=%s", resp.StatusCode, sanitizeForLog(string(b))) + return "", fmt.Errorf("openai api error: status %d: %s", resp.StatusCode, sanitizeForLog(string(b))) + } + var parsed map[string]interface{} + if err := json.Unmarshal(b, &parsed); err != nil { + return strings.TrimSpace(string(b)), nil + } + if choices, ok := parsed["choices"].([]interface{}); ok && len(choices) > 0 { + if ch0, ok := choices[0].(map[string]interface{}); ok { + if msg, ok := ch0["message"].(map[string]interface{}); ok { + if content, ok := msg["content"].(string); ok { + return strings.TrimSpace(content), nil + } + } + if txt, ok := ch0["text"].(string); ok { + return strings.TrimSpace(txt), nil + } + } + } + return strings.TrimSpace(string(b)), nil + } + + return "", fmt.Errorf("no AI API key configured (set GOOGLE_API_KEY or OPENAI_API_KEY)") +} + +// helper functions +func sanitizeForLog(s string) string { + if s == "" { + return "" + } + if len(s) <= 800 { + return s + } + return s[:800] + "...[truncated]" +} + +func findFirstString(v interface{}) string { + switch t := v.(type) { + case string: + return t + case []interface{}: + for _, e := range t { + if s := findFirstString(e); s != "" { + return s + } + } + case map[string]interface{}: + for _, val := range t { + if s := findFirstString(val); s != "" { + return s + } + } + } + return "" +} diff --git a/modules/gpt.go.bak b/modules/gpt.go.bak new file mode 100644 index 0000000..7558644 --- /dev/null +++ b/modules/gpt.go.bak @@ -0,0 +1,152 @@ +package modules + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" +) + +// ForwardToOpenAI sends prompt to Google Generative Language (Gemini) using +// the generateContent endpoint (v1beta). It expects env: +// GOOGLE_API_KEY (your API key) +// GOOGLE_MODEL (e.g. gemini-2.5-flash or models/gemini-2.5-flash — both accepted) +// Returns generated text or detailed error (includes response body). +func ForwardToOpenAI(prompt string) (string, error) { + key := os.Getenv("GOOGLE_API_KEY") + if key == "" { + return "", fmt.Errorf("GOOGLE_API_KEY not set in environment") + } + + model := os.Getenv("GOOGLE_MODEL") + if model == "" { + model = "gemini-2.5-flash" + } + // tolerate "models/" prefix if user included it + model = trimModelsPrefix(model) + + // Use the v1beta generateContent endpoint (more compatible for Gemini) + url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", model, key) + + // Request body using contents array (text-only) + reqBody := map[string]interface{}{ + "model": model, + "contents": []interface{}{ + map[string]interface{}{ + "type": "text", + "text": prompt, + }, + }, + // optional tuning: + "temperature": 0.2, + "maxOutputTokens": 256, + } + + b, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("marshal request body: %w", err) + } + + client := &http.Client{Timeout: 25 * time.Second} + req, err := http.NewRequest("POST", url, bytes.NewReader(b)) + if err != nil { + return "", fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("http request failed: %w", err) + } + defer resp.Body.Close() + + bodyBytes, _ := io.ReadAll(resp.Body) + bodyStr := string(bodyBytes) + + if resp.StatusCode == http.StatusNotFound { + // Surface full details to help debug model / access problems + return "", fmt.Errorf("google api 404 Not Found. url=%s status=%d body=%s", url, resp.StatusCode, bodyStr) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("google api error: status %d: %s", resp.StatusCode, bodyStr) + } + + // Parse tolerant - try outputs / candidates / etc. + var parsed map[string]interface{} + if err := json.Unmarshal(bodyBytes, &parsed); err != nil { + return "", fmt.Errorf("failed to parse google response: %w; raw: %s", err, bodyStr) + } + + // Try common shapes: outputs -> content -> text; candidates -> content/text + // outputs -> content[0].text + if outs, ok := parsed["outputs"].([]interface{}); ok && len(outs) > 0 { + if first, ok := outs[0].(map[string]interface{}); ok { + if content, ok := first["content"].([]interface{}); ok && len(content) > 0 { + if c0, ok := content[0].(map[string]interface{}); ok { + if txt, ok := c0["text"].(string); ok && txt != "" { + return txt, nil + } + } + } + if txt, ok := first["text"].(string); ok && txt != "" { + return txt, nil + } + } + } + + // candidates -> [0].content or [0].text + if cands, ok := parsed["candidates"].([]interface{}); ok && len(cands) > 0 { + if c0, ok := cands[0].(map[string]interface{}); ok { + if v, ok := c0["content"].(string); ok && v != "" { + return v, nil + } + if v, ok := c0["text"].(string); ok && v != "" { + return v, nil + } + } + } + + // fallback: attempt to find any nested string + if s := findAnyString(parsed); s != "" { + return s, nil + } + + // final fallback: return raw body (caller can log) + return bodyStr, nil +} + +func trimModelsPrefix(m string) string { + if len(m) >= 7 && (m[:7] == "models/" || m[:7] == "/models") { + // remove leading "models/" if present + if m[:1] == "/" { + m = m[1:] + } + if len(m) > 7 { + return m[7:] + } + } + return m +} + +func findAnyString(v interface{}) string { + switch vv := v.(type) { + case string: + return vv + case map[string]interface{}: + for _, val := range vv { + if s := findAnyString(val); s != "" { + return s + } + } + case []interface{}: + for _, it := range vv { + if s := findAnyString(it); s != "" { + return s + } + } + } + return "" +} diff --git a/modules/hype.go b/modules/hype.go new file mode 100644 index 0000000..41db0dc --- /dev/null +++ b/modules/hype.go @@ -0,0 +1,19 @@ +package modules + +import ( + "strings" +) + +// RunHype is the public entry used by the agent to get a hype reply. +func RunHype(args []string) (string, error) { + if len(args) == 0 { + return "Usage: hype [token]. Example: hype sol", nil + } + token := strings.TrimSpace(args[0]) + if token == "" { + return "Usage: hype [token]. Example: hype sol", nil + } + + reply := BuildHypeReply(token) + return reply, nil +} diff --git a/modules/market_handlers.go b/modules/market_handlers.go new file mode 100644 index 0000000..525ce4d --- /dev/null +++ b/modules/market_handlers.go @@ -0,0 +1,157 @@ +package modules + +import ( + "fmt" + "os" + "strings" +) + +const replyTimeLayout = "2006-01-02 15:04:05 MST" + +// BuildHypeReply returns a human-friendly hype summary for a symbol. +func BuildHypeReply(symbol string) string { + sym := strings.TrimSpace(symbol) + if sym == "" { + return "Hype: unknown symbol" + } + if strings.ToLower(os.Getenv("MOCK_MODE")) == "true" { + return fmt.Sprintf("Hype score for $%s: 0.00\nTrend: Trend snapshot for %s (mock): bullish momentum, strong volume spikes\n24h Move: 0.00%%", strings.ToUpper(sym), strings.ToUpper(sym)) + } + + md, err := GetMarketData(sym) + if err != nil { + return fmt.Sprintf("Hype score for $%s: (data unavailable). Reason: %v", strings.ToUpper(sym), summarizeErr(err)) + } + + score := ComputeHypeScore(md) + + trend := "neutral" + if md.Change24h >= 2.0 { + trend = "bullish" + } else if md.Change24h <= -2.0 { + trend = "bearish" + } + + reply := fmt.Sprintf( + "Hype score for $%s: %.2f\nTrend: %s (24h change: %.2f%%)\nPrice: $%0.6f • 24h Volume: $%.0f • MarketCap: $%.0f\nData as of: %s", + strings.ToUpper(sym), + score, + strings.Title(trend), + md.Change24h, + md.PriceUSD, + md.Volume24h, + md.MarketCapUSD, + md.RetrievedAt.Format(replyTimeLayout), + ) + return reply +} + +// BuildSentimentReply returns a simple sentiment summary for a token. +func BuildSentimentReply(symbol string) string { + sym := strings.TrimSpace(symbol) + if sym == "" { + return "Sentiment: unknown symbol" + } + if strings.ToLower(os.Getenv("MOCK_MODE")) == "true" { + return fmt.Sprintf("Sentiment for $%s:\nšŸ‘ 0.0%% positive\nšŸ‘Ž 0.0%% negative", strings.ToUpper(sym)) + } + + md, err := GetMarketData(sym) + if err != nil { + return fmt.Sprintf("Sentiment for $%s: (data unavailable). Reason: %v", strings.ToUpper(sym), summarizeErr(err)) + } + + pos := 0.0 + neg := 0.0 + if md.Change24h > 1.0 { + pos = 75.0 + neg = 25.0 + } else if md.Change24h < -1.0 { + pos = 25.0 + neg = 75.0 + } else { + pos = 50.0 + neg = 50.0 + } + + return fmt.Sprintf("Sentiment for $%s:\nšŸ‘ %.1f%% positive\nšŸ‘Ž %.1f%% negative\nPrice: $%.6f (24h: %+0.2f%%)", + strings.ToUpper(sym), pos, neg, md.PriceUSD, md.Change24h) +} + +// BuildRiskReply returns a small risk-check summary. +func BuildRiskReply(symbol string) string { + sym := strings.TrimSpace(symbol) + if sym == "" { + return "Risk: unknown symbol" + } + if strings.ToLower(os.Getenv("MOCK_MODE")) == "true" { + return fmt.Sprintf("Risk check for $%s:\n- RiskScore: 0.30\n- Indicators:\n - Very low market cap", strings.ToUpper(sym)) + } + + md, err := GetMarketData(sym) + if err != nil { + return fmt.Sprintf("Risk check for $%s: (data unavailable). Reason: %v", strings.ToUpper(sym), summarizeErr(err)) + } + + score := 0.0 + if md.MarketCapUSD <= 0 { + score = 0.9 + } else { + mc := md.MarketCapUSD + if mc < 1_000_000 { + score = 0.85 + } else if mc < 10_000_000 { + score = 0.6 + } else if mc < 100_000_000 { + score = 0.4 + } else if mc < 1_000_000_000 { + score = 0.25 + } else { + score = 0.12 + } + } + if md.Change24h > 5 || md.Change24h < -5 { + score = score + 0.15 + } + if score > 1 { + score = 1 + } + + indicators := []string{} + if md.MarketCapUSD < 10_000_000 { + indicators = append(indicators, "Very low market cap") + } + if md.Volume24h < 10_000 { + indicators = append(indicators, "Very low volume") + } + if md.Change24h > 5 { + indicators = append(indicators, "Large positive price spike (24h)") + } + if md.Change24h < -5 { + indicators = append(indicators, "Large negative price drop (24h)") + } + if len(indicators) == 0 { + indicators = append(indicators, "No immediate red flags") + } + + reply := fmt.Sprintf("Risk check for $%s:\n- RiskScore: %.2f\n- Indicators:\n - %s\nPrice: $%.6f • MarketCap: $%.0f • 24h: %+0.2f%%", + strings.ToUpper(sym), + score, + strings.Join(indicators, "\n - "), + md.PriceUSD, + md.MarketCapUSD, + md.Change24h, + ) + return reply +} + +func summarizeErr(err error) string { + if err == nil { + return "" + } + s := err.Error() + if len(s) > 200 { + return s[:200] + "..." + } + return s +} diff --git a/modules/risk.go b/modules/risk.go new file mode 100644 index 0000000..df6be41 --- /dev/null +++ b/modules/risk.go @@ -0,0 +1,19 @@ +package modules + +import ( + "strings" +) + +// RunRiskCheck is the public entry used by the agent to perform risk checks. +func RunRiskCheck(args []string) (string, error) { + if len(args) == 0 { + return "Usage: riskcheck [token]. Example: riskcheck sol", nil + } + token := strings.TrimSpace(args[0]) + if token == "" { + return "Usage: riskcheck [token]. Example: riskcheck sol", nil + } + + reply := BuildRiskReply(token) + return reply, nil +} diff --git a/modules/scan.go b/modules/scan.go new file mode 100644 index 0000000..4990800 --- /dev/null +++ b/modules/scan.go @@ -0,0 +1,24 @@ +package modules + +import ( + "fmt" + "strings" +) + +// RunScan performs a simple mock scan for a token and returns a human-readable summary. +func RunScan(args []string) (string, error) { + if len(args) == 0 { + return "Usage: scan [token]. Example: scan SOL", nil + } + token := strings.ToUpper(args[0]) + // Mocked analysis + hype := 72 + sentiment := "mixed" + kols := []string{"Ansem", "GCR", "TheMoonCarl"} + risk := "moderate" + + res := fmt.Sprintf("Scan result for %s:\n- HypeScore: %d/100\n- Sentiment: %s\n- Top KOL mentions: %d (%s)\n- RiskFlag: %s", + token, hype, sentiment, len(kols), strings.Join(kols, ", "), risk) + + return res, nil +} diff --git a/modules/sentiment.go b/modules/sentiment.go new file mode 100644 index 0000000..c56d690 --- /dev/null +++ b/modules/sentiment.go @@ -0,0 +1,19 @@ +package modules + +import ( + "strings" +) + +// RunSentiment provides the public entry used by the agent to return sentiment. +func RunSentiment(args []string) (string, error) { + if len(args) == 0 { + return "Usage: sentiment [token]", nil + } + token := strings.TrimSpace(args[0]) + if token == "" { + return "Usage: sentiment [token]", nil + } + + reply := BuildSentimentReply(token) + return reply, nil +} diff --git a/modules/summary.go b/modules/summary.go new file mode 100644 index 0000000..3dba890 --- /dev/null +++ b/modules/summary.go @@ -0,0 +1,5 @@ +package modules + +func RunSummary() (string, error) { + return "Daily summary: 5 signals detected, 2 risky tokens, 3 trending coins (mock).", nil +} diff --git a/modules/topcalls.go b/modules/topcalls.go new file mode 100644 index 0000000..59610f0 --- /dev/null +++ b/modules/topcalls.go @@ -0,0 +1,12 @@ +package modules + +import "fmt" + +func RunTopCalls() (string, error) { + return fmt.Sprintf( +`Latest KOL Early Calls: +1. SOL – Mentioned by Ansem +2. ETH – Mentioned by SatoshiLite +3. DOGE – Mentioned by TheMoonCarl +`, ), nil +} diff --git a/modules/utils.go b/modules/utils.go new file mode 100644 index 0000000..5297b04 --- /dev/null +++ b/modules/utils.go @@ -0,0 +1,44 @@ +package modules + +import ( + "encoding/json" + "fmt" + "strconv" +) + +// safeGetFloat navigates nested JSON from CoinGecko safely +func safeGetFloat(m map[string]interface{}, path ...string) float64 { + cur := interface{}(m) + for i, p := range path { + if mm, ok := cur.(map[string]interface{}); ok { + v, exists := mm[p] + if !exists { + return 0 + } + if i == len(path)-1 { + switch x := v.(type) { + case float64: + return x + case int: + return float64(x) + case int64: + return float64(x) + case string: + f, _ := strconv.ParseFloat(x, 64) + return f + default: + return 0 + } + } + cur = v + } else { + return 0 + } + } + return 0 +} + +func DebugLog(label string, v interface{}) { + b, _ := json.MarshalIndent(v, "", " ") + fmt.Printf("[DEBUG] %s: %s\n", label, string(b)) +} diff --git a/modules/watch.go b/modules/watch.go new file mode 100644 index 0000000..1ca9baa --- /dev/null +++ b/modules/watch.go @@ -0,0 +1,11 @@ +package modules + +import "fmt" + +func RunWatch(args []string) (string, error) { + if len(args) == 0 { + return "Usage: watch [KOL]", nil + } + kol := args[0] + return fmt.Sprintf("Now watching KOL: %s (mock). Alerts will be generated on significant activity.", kol), nil +} diff --git a/modules/xscanner.go b/modules/xscanner.go new file mode 100644 index 0000000..3444f86 --- /dev/null +++ b/modules/xscanner.go @@ -0,0 +1,72 @@ +package modules + +import ( + "context" + "fmt" + "log" + "math/rand" + "strings" + "time" +) + +// StartXScanner runs a scanner loop. It sends detections into out channel +// signature: +// ctx context.Context +// intervalSec int +// kols []string +// bearer string (for real mode; if empty, stay in mock mode) +// source string +// mock bool +// out chan<- Detection +func StartXScanner(ctx context.Context, intervalSec int, kols []string, bearer string, source string, mock bool, out chan<- Detection) { + log.Printf("[xscanner] Starting scanner (mock=%v, interval=%ds, KOLs=%v, source=%s)", mock, intervalSec, kols, source) + ticker := time.NewTicker(time.Duration(intervalSec) * time.Second) + rand.Seed(time.Now().UnixNano()) + + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + log.Println("[xscanner] Stopped.") + return + case <-ticker.C: + // produce one mock detection per tick when mock==true + if mock { + d := generateMockDetection(kols, source) + if d.Text != "" { + out <- d + } + continue + } + + // REAL mode placeholder: not implemented (must use bearer token) + if bearer == "" { + log.Println("[xscanner] WARNING: real mode requested but no bearer token provided; skipping") + continue + } + + // TODO: implement real fetch using X/Twitter API with rate-limits and parsing + log.Println("[xscanner] real mode requested but not implemented yet.") + } + } +} + +func generateMockDetection(kols []string, source string) Detection { + if len(kols) == 0 { + return Detection{} + } + kol := kols[rand.Intn(len(kols))] + tokenList := []string{"SOL", "BTC", "DOGE", "PEPE", "BONK", "ABC"} + token := tokenList[rand.Intn(len(tokenList))] + msg := fmt.Sprintf("KOL %s mentioned %s", kol, token) + link := "https://twitter.com/" + strings.ToLower(kol) + return Detection{ + Text: msg, + Link: link, + Source: source, + Timestamp: TimeNowUTC(), + KOL: kol, + Token: token, + Confidence: rand.Float64()*0.6 + 0.4, + } +} From 665402bce78720c091812bfae31db4e0ab0e3a6a Mon Sep 17 00:00:00 2001 From: m00se <89501537+Mooseiftime@users.noreply.github.com> Date: Mon, 1 Dec 2025 04:49:10 +0700 Subject: [PATCH 9/9] Delete modules/.keep --- modules/.keep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 modules/.keep diff --git a/modules/.keep b/modules/.keep deleted file mode 100644 index 48cdce8..0000000 --- a/modules/.keep +++ /dev/null @@ -1 +0,0 @@ -placeholder