├── .editorconfig ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc ├── .vscode └── extensions.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── config.md ├── npm-publishing.md ├── problem-solution.md └── project-documentation.md ├── package.json ├── scripts ├── create-default-config.js ├── gen-config-schema.ts ├── publish-npm.bat └── publish-npm.sh ├── src ├── agents │ ├── code-writer.ts │ ├── coordinator.ts │ ├── functionality.ts │ ├── index.ts │ ├── linting.ts │ ├── security-audit.ts │ ├── vector-store.ts │ └── web-search.ts ├── cli.ts ├── cli │ ├── commands │ │ ├── builtin-commands.ts │ │ ├── contract.ts │ │ ├── generate.ts │ │ └── vector-db.ts │ └── index.ts ├── services │ ├── ai │ │ ├── ai-command.ts │ │ ├── ai-sdk.ts │ │ ├── ask.ts │ │ ├── client.ts │ │ ├── mastra-shim.ts │ │ ├── models.ts │ │ └── rag-utils.ts │ ├── config │ │ └── config.ts │ ├── contract │ │ ├── agent-mode.ts │ │ ├── contract-commands.ts │ │ ├── contract-utils.ts │ │ ├── explain-contract.ts │ │ ├── generate-contract.ts │ │ ├── generator.ts │ │ └── metamask-errors.ts │ ├── search │ │ └── search.ts │ ├── ui │ │ └── chat.ts │ └── vector-db │ │ └── vector-db.ts └── utils │ ├── common.ts │ ├── error.ts │ ├── fetch-url.ts │ ├── logger.ts │ ├── markdown.ts │ └── tty.ts ├── tsconfig.json ├── tsup.config.ts ├── types.d.ts ├── web3ailogo.png └── web3cli.example.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | dist/ 3 | build/ 4 | 5 | # Dependencies 6 | node_modules/ 7 | package-lock.json 8 | .pnp.* 9 | .yarn/* 10 | 11 | # Environment variables and secrets 12 | .env 13 | .env.local 14 | .env.development 15 | .env.test 16 | .env.production 17 | 18 | # Operating System files 19 | .DS_Store 20 | Thumbs.db 21 | desktop.ini 22 | 23 | # IDE/Editor files 24 | .vscode/* 25 | !.vscode/extensions.json 26 | .idea/ 27 | *.sublime-project 28 | *.sublime-workspace 29 | *.code-workspace 30 | 31 | # Logs 32 | logs/ 33 | *.log 34 | npm-debug.log* 35 | yarn-debug.log* 36 | yarn-error.log* 37 | 38 | # Testing 39 | coverage/ 40 | .nyc_output/ 41 | 42 | # Temporary files 43 | tmp/ 44 | .tmp/ 45 | *.tmp 46 | *.swp 47 | *.swo 48 | *.sample 49 | 50 | # Generated files 51 | schema.json 52 | 53 | # Terminal AI 54 | terminal-ai.toml 55 | paper.tex 56 | web3cli.toml 57 | .vector-db/ 58 | .output/ 59 | .Lib/ 60 | .husky/ 61 | 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development files 2 | .git 3 | .github 4 | .vscode 5 | .editorconfig 6 | .gitignore 7 | .prettierrc 8 | tsconfig.json 9 | tsup.config.ts 10 | .eslintrc.js 11 | .prettier* 12 | .npmrc 13 | 14 | # Scripts and docs 15 | scripts/ 16 | docs/ 17 | 18 | # Source code (since we're distributing compiled code) 19 | src/ 20 | 21 | # Test files 22 | test/ 23 | *.test.ts 24 | *.spec.ts 25 | 26 | # Other non-essential files 27 | .vector-db/ 28 | output/ 29 | node_modules/ 30 | examples/ 31 | *.log 32 | npm-debug.log* 33 | pnpm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | .pnpm-store/ 37 | 38 | # Example files 39 | web3cli.example.toml 40 | terminal-ai.toml 41 | 42 | # Include certain files explicitly 43 | !dist/ 44 | !schema.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | save-exact=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tamasfe.even-better-toml"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to the @web3ai/cli package will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2025-05-10 9 | 10 | ### Added 11 | - Multi-Provider AI Support (OpenAI, Anthropic/Claude, Google Gemini, Groq, Mistral, GitHub Copilot, and Ollama) 12 | - Natural Language to Solidity Code generation 13 | - Security-First approach with built-in guardrails 14 | - Contract Explainability 15 | - Multi-Agent System for enhanced quality 16 | - Vector Database for local storage of documentation and security patterns 17 | - Web Search capabilities 18 | - CLI and Terminal Interface 19 | - Robust File System Handling - automatically creates necessary directories 20 | - Support for multiple AI models across different providers 21 | 22 | ### Changed 23 | - Initial npm package release 24 | 25 | ## [1.1.0] - 2025-05-10 26 | 27 | ### Added 28 | - Improved File System Handling - Now automatically creates output directories as needed 29 | - Enhanced Error Handling - Better error messages for common issues 30 | - Model Selection Improvements - Simplified model selection and provider detection 31 | - MetaMask Error Handling Utility 32 | 33 | ## [1.1.2] - 2025-05-11 34 | 35 | ### Added 36 | - Initial GitHub repository version 37 | - Core functionality for Solidity smart contract generation 38 | 39 | ## [1.1.3] - 2025-05-11 40 | 41 | ### Added 42 | - Officially added MIT License. 43 | - added uups-proxy and transparent-proxy 44 | 45 | ## [1.1.6] - 2025-05-12 46 | 47 | ### Added 48 | - change the default package manager to npm 49 | - Fixed agent mode 50 | 51 | ## [1.1.7] - 2025-05-12 52 | 53 | ### Added 54 | - config Update -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 shivatmax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web3CLI: AI-Powered Smart Contract Tool 2 | 3 | 4 | [![npm version](https://badge.fury.io/js/%40web3ai%2Fcli.svg)](https://badge.fury.io/js/%40web3ai%2Fcli) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | 8 | A comprehensive suite for generating secure Solidity smart contracts and analyzing existing contracts using AI. 9 | 10 |

11 | Web3ai logo 12 |

13 | 14 | ## Table of Contents 15 | - [Overview](#overview) 16 | - [Installation](#installation) 17 | - [Configuration](#configuration) 18 | - [Key Features](#key-features) 19 | - [Supported AI Models](#supported-ai-models) 20 | - [Usage Examples](#usage-examples) 21 | - [Agent Mode](#agent-mode) 22 | - [Options](#options) 23 | - [Vector Database](#vector-database) 24 | - [Design Tradeoffs](#design-tradeoffs) 25 | - [Project Structure](#project-structure) 26 | - [Core Technologies](#core-technologies) 27 | - [Recent Updates](#recent-updates) 28 | - [Future Plans](#future-plans) 29 | - [Contributing](#contributing) 30 | - [Community & Support](#community--support) 31 | - [License](#license) 32 | 33 | ## Overview 34 | 35 | Web3CLI is a powerful developer tool that leverages AI to solve two key problems in blockchain development: 36 | 37 | 1. **Natural Language to Smart Contract Logic** - Translates requirements into secure, minimal Solidity code 38 | 2. **Smart Contract Explainability** - Provides plain-English summaries of complex smart contracts 39 | 40 | The tool uses advanced AI models with specialized guardrails to ensure secure code generation, supported by a multi-agent system, web search, and vector database integration for enhanced security and quality. 41 | 42 | ## Installation 43 | 44 | ### NPM Package 45 | 46 | The easiest way to install Web3CLI is via npm: 47 | 48 | ```bash 49 | npm install -g @web3ai/cli 50 | ``` 51 | 52 | Or with pnpm: 53 | 54 | ```bash 55 | pnpm add -g @web3ai/cli 56 | ``` 57 | 58 | ### Manual Installation 59 | 60 | If you prefer to install from source: 61 | 62 | ```bash 63 | git clone https://github.com/shivatmax/web3cli.git 64 | ``` 65 | ```bash 66 | cd web3cli 67 | ``` 68 | ```bash 69 | pnpm install 70 | ``` 71 | ```bash 72 | pnpm build 73 | ``` 74 | ```bash 75 | npm link 76 | ``` 77 | 78 | ## Configuration 79 | 80 | Create a `web3cli.toml` file in your project directory: 81 | 82 | ```toml 83 | #:schema ./schema.json 84 | default_model = "gpt-4o-mini" # or another model 85 | 86 | # OpenAI Configuration 87 | openai_api_key = "your-openai-api-key" 88 | 89 | # Gemini Configuration - for Google Gemini models 90 | # gemini_api_key = "your-gemini-api-key" 91 | 92 | # Anthropic Configuration - for Claude models 93 | # anthropic_api_key = "your-anthropic-api-key" 94 | 95 | # Groq Configuration - for faster inference 96 | # groq_api_key = "your-groq-api-key" 97 | 98 | # Mistral Configuration 99 | # mistral_api_key = "your-mistral-api-key" 100 | 101 | # Ollama Configuration - for local models 102 | # ollama_host = "http://localhost:11434" 103 | 104 | # Etherscan API key (optional, for contract analysis) 105 | etherscan_api_key = "your-etherscan-api-key" 106 | ``` 107 | 108 | Or set environment variables: 109 | - `OPENAI_API_KEY` - For OpenAI models 110 | - `GEMINI_API_KEY` - For Google Gemini models 111 | - `ANTHROPIC_API_KEY` - For Claude models 112 | - `GROQ_API_KEY` - For Groq inference 113 | - `MISTRAL_API_KEY` - For Mistral models 114 | - `ETHERSCAN_API_KEY` - For contract analysis (optional) 115 | 116 | ## Key Features 117 | 118 | - **Multi-Provider AI Support** - Works with OpenAI, Anthropic/Claude, Google Gemini, Groq, Mistral, GitHub Copilot, and Ollama 119 | - **Natural Language to Solidity Code** - Generate smart contracts from plain English 120 | - **Security-First Approach** - Built-in guardrails to prevent insecure patterns 121 | - **Contract Explainability** - Analyze contracts for permissions and security patterns 122 | - **Multi-Agent System** - Specialized agents collaborate to enhance quality 123 | - **Vector Database** - Local storage of blockchain documentation and security patterns 124 | - **Web Search** - Up-to-date information for secure implementations 125 | - **CLI and Terminal Interface** - Developer-friendly command-line tools 126 | - **Robust File System Handling** - Automatically creates necessary directories for output 127 | 128 | ## Supported AI Models 129 | 130 | Web3CLI supports a wide range of AI models across multiple providers: 131 | 132 | ### OpenAI 133 | - GPT-4o, GPT-4o-mini, GPT-4.1, GPT-3.5-turbo 134 | - OpenAI "o" series: o1, o1-mini, o3, o4-mini, etc. 135 | 136 | ### Anthropic 137 | - Claude 3.7 Sonnet, Claude 3.5 Sonnet, Claude 3.5 Haiku, Claude 3 Opus 138 | 139 | ### Google Gemini 140 | - Gemini 2.5 Flash, Gemini 2.5 Pro, Gemini 2.0, Gemini 1.5 141 | 142 | ### Groq 143 | - Llama 3.3 70B, Llama 3.1 8B, Mixtral 8x7B 144 | 145 | ### Mistral 146 | - Mistral Large, Mistral Medium, Mistral Small 147 | 148 | ### GitHub Copilot 149 | - Copilot models with GPT-4o, o1, Claude 3.5 backend options 150 | 151 | ### Ollama 152 | - Local models via Ollama server 153 | 154 | ## Usage Examples 155 | 156 | ### General Web3 Development Questions 157 | 158 | ```bash 159 | # Ask a general Web3 development question 160 | web3cli "What is the difference between ERC-20 and ERC-721?" 161 | 162 | # Ask with web search enabled 163 | web3cli "What is the current gas cost for token transfers?" --search 164 | 165 | # Ask with specific model 166 | web3cli "Explain the EIP-2981 royalty standard" --model gpt-4o 167 | 168 | # Ask with alternative providers 169 | web3cli "Explain the EIP-2981 royalty standard" --model claude-3-5-sonnet 170 | web3cli "Explain gas optimization" --model gemini-2.5-flash 171 | 172 | # List available models 173 | web3cli list 174 | ``` 175 | 176 | ### Natural Language to Smart Contract 177 | 178 | ```bash 179 | # Generate an upgradeable ERC-20 using Transparent Proxy 180 | web3cli generate "Create an ERC-20 token with upgradeability" --transparent-proxy --output MyToken.sol --no-stream 181 | 182 | # Generate an NFT collection using UUPS proxy pattern with agent mode 183 | web3cli generate "Create an NFT collection with royalties and upgradeability" --uups-proxy --agent --output NFTCollection.sol --no-stream 184 | 185 | # Generate with agent mode for enhanced security 186 | web3cli generate "Create an ERC-20 token with minting restricted to addresses in an allowlist" --agent --output Token.sol --no-stream 187 | 188 | # Generate with Hardhat tests 189 | web3cli generate "Create an NFT collection with royalties" --hardhat --output NFTCollection.sol --no-stream 190 | 191 | # Generate with web search for security best practices 192 | web3cli generate "Create a vesting contract" --search --no-stream 193 | 194 | # Generate with vector DB context 195 | web3cli generate "Create an NFT with royalties" --read-docs solidity 196 | ``` 197 | 198 | ### Smart Contract Explainability 199 | 200 | ```bash 201 | # Analyze a contract by address (Mainnet) 202 | web3cli contract 0xdac17f958d2ee523a2206206994597c13d831ec7 --network mainnet -o 203 | 204 | # Analyze a Solidity file 205 | web3cli contract --file MyContract.sol --no-stream 206 | 207 | # Explain a Solidity file 208 | web3cli contract:explain --file MyContract.sol --no-stream 209 | 210 | # Audit a contract 211 | web3cli contract:audit 0xdac17f958d2ee523a2206206994597c13d831ec7 --network mainnet -o 212 | 213 | # Ask custom questions about a contract 214 | web3cli contract:custom 0xdac17f958d2ee523a2206206994597c13d831ec7 "What security patterns does this contract implement?" --network mainnet 215 | ``` 216 | 217 | ## Agent Mode 218 | 219 | When using the agent mode with `--agent` flag, the system follows this workflow: 220 | 221 | 1. The **Coordinator Agent** receives the natural language request and plans the execution 222 | 2. The **Web Search Agent** gathers relevant information about the requested contract if needed 223 | 3. The **Vector Store Agent** retrieves security patterns and best practices from the vector database 224 | 4. The **Code Writer Agent** generates the initial Solidity implementation using all gathered context 225 | 5. The **Security Audit Agent** analyzes the code for vulnerabilities and provides improvements 226 | 6. The **Linting Agent** cleans up the code style and improves readability 227 | 7. The **Functionality Checker** verifies the contract works as intended and generates tests if requested 228 | 8. The **Coordinator Agent** finalizes the output, combining all the improvements 229 | 230 | This collaborative approach results in higher quality, more secure smart contracts than using a single AI model. 231 | 232 | ## Options 233 | 234 | - `--model `: Specify the model to use (default: gpt-4o-mini) 235 | - `--output `: Output file for the generated contract 236 | - `--hardhat`: Generate Hardhat test file 237 | - `--agent`: Use hierarchical multi-agent mode 238 | - `--transparent-proxy`: Generate an upgradeable contract using the Transparent Proxy pattern (OpenZeppelin) 239 | - `--uups-proxy`: Generate an upgradeable contract using the UUPS pattern (OpenZeppelin) 240 | - `--files `: Additional context files 241 | - `--url `: URLs to fetch as context 242 | - `--search`: Enable web search for context 243 | - `--read-docs `: Read from vector DB docs collection 244 | - `--no-stream`: Disable streaming responses 245 | 246 | ## Vector Database 247 | 248 | Web3CLI includes a local vector database for storing and searching documentation using semantic similarity. 249 | 250 | ### Vector Database Commands 251 | 252 | ```bash 253 | # List all collections in the vector database 254 | web3cli vdb-list 255 | 256 | # Add documents from a URL to the vector database 257 | web3cli vdb-add-docs --name --crawl --max-pages 30 258 | 259 | # Add a file to the vector database 260 | web3cli vdb-add-file --name --title "Document Title" 261 | 262 | # Search the vector database 263 | web3cli vdb-search "ERC721 royalties implementation" --name solidity -k 5 264 | 265 | # Add documentation from predefined sources 266 | web3cli setup --max-pages 50 267 | 268 | # Initialize vector database (alias for backward compatibility) 269 | web3cli vector-db 270 | 271 | # Use vector search with generation 272 | web3cli generate "Create an NFT with royalties" --read-docs solidity 273 | ``` 274 | 275 | ### Document Structure 276 | 277 | The vector database stores and returns documents with this structure: 278 | 279 | ```typescript 280 | { 281 | pageContent: "The document text content...", 282 | metadata: { 283 | source: "https://example.com/docs/page", 284 | title: "Document Title", 285 | url: "https://example.com/docs/page", 286 | siteName: "Example Documentation", 287 | author: "Example Author", 288 | crawlTime: "2023-06-15T12:34:56Z" 289 | } 290 | } 291 | ``` 292 | 293 | ## Design Tradeoffs 294 | 295 | ### Model Choice 296 | - Multiple AI providers supported for flexibility and performance 297 | - **OpenAI/GPT-4o** - Superior understanding of Solidity but higher cost 298 | - **Claude models** - Strong reasoning for complex contracts 299 | - **Gemini models** - Good balance of capabilities and cost 300 | - **Groq models** - Fast inference for time-sensitive tasks 301 | - **Mistral models** - Efficient performance for routine tasks 302 | - **Ollama** - Local models for privacy and offline work 303 | - Security is prioritized over cost for critical smart contract generation 304 | - Lesser models used for non-critical tasks like search and documentation 305 | 306 | ### Security vs. Speed 307 | - Security is prioritized with multiple agent reviews 308 | - Vector database provides security patterns for faster reference 309 | - Tradeoff favors security at the cost of generation time 310 | 311 | ## Project Structure 312 | 313 | ``` 314 | web3cli/ 315 | ├── docs/ # Documentation 316 | ├── scripts/ # Utility scripts 317 | ├── src/ # Source code 318 | │ ├── agents/ # Agent system components 319 | │ │ ├── coordinator.ts # Agent orchestration 320 | │ │ ├── code-writer.ts # Code generation 321 | │ │ ├── security-audit.ts # Security auditing 322 | │ │ ├── linting.ts # Code quality 323 | │ │ ├── functionality.ts # Verify behavior 324 | │ │ ├── web-search.ts # Web search 325 | │ │ └── vector-store.ts # Documentation retrieval 326 | │ ├── cli/ # CLI interface 327 | │ │ └── commands/ # Command implementations 328 | │ ├── services/ # Core services 329 | │ │ ├── ai/ # AI model integration 330 | │ │ ├── config/ # Configuration 331 | │ │ ├── contract/ # Contract generation 332 | │ │ ├── search/ # Search services 333 | │ │ ├── ui/ # User interface helpers 334 | │ │ └── vector-db/ # Vector database 335 | │ └── utils/ # Shared utilities 336 | ``` 337 | 338 | ## Core Technologies 339 | 340 | Web3CLI is built with the following key technologies: 341 | 342 | - **Multiple AI Providers** - OpenAI, Anthropic, Google, Groq, Mistral, GitHub Copilot, and Ollama 343 | - **LangChain** - Framework for multi-agent operations 344 | - **OpenAI Embeddings** - For vector representation 345 | - **ethers.js** - Ethereum interaction library 346 | - **Solidity Compiler** - For validating contracts 347 | - **CAC** - Lightweight CLI framework 348 | 349 | ## Recent Updates 350 | 351 | - **Multi-Provider AI Support** - Added support for Claude, Gemini, Groq, Mistral, GitHub Copilot, and Ollama models 352 | - **Improved File System Handling** - Now automatically creates output directories as needed 353 | - **Enhanced Error Handling** - Better error messages for common issues 354 | - **Model Selection Improvements** - Simplified model selection and provider detection 355 | - **MetaMask Error Handling Utility** - Added support for better MetaMask error handling 356 | 357 | See the [CHANGELOG.md](CHANGELOG.md) for a full history of changes. 358 | 359 | ## Future Plans 360 | 361 | 1. **Formal Verification Integration** - Connect with formal verification tools for critical contracts 362 | 2. **Gas Optimization Analysis** - Add detailed gas estimation and optimization suggestions 363 | 3. **Custom Documentation Integration** - Allow developers to add proprietary documentation 364 | 4. **Web Interface** - Develop a web-based UI for easier adoption 365 | 5. **Expanded Chain Support** - Add support for additional EVM-compatible chains 366 | 367 | ## Contributing 368 | 369 | We welcome contributions to Web3CLI! If you're interested in helping, please: 370 | 1. Fork the repository. 371 | 2. Create a new branch for your feature or bug fix. 372 | 3. Make your changes. 373 | 4. Ensure your code adheres to the project's linting and formatting standards. 374 | 5. Submit a pull request with a clear description of your changes. 375 | 376 | Please check our [issues page](https://github.com/shivatmax/web3cli/issues) for areas where you can contribute. 377 | 378 | ## Community & Support 379 | 380 | - **Questions & Discussions:** For general questions, discussions, or support, please open an issue on our [GitHub Issues page](https://github.com/shivatmax/web3cli/issues). 381 | - **Bug Reports:** If you find a bug, please report it by creating an issue, providing as much detail as possible. 382 | 383 | ## License 384 | 385 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details (though your project uses the standard MIT text, so a separate LICENSE file might not be strictly necessary if you clearly state "MIT" in `package.json` and here). 386 | 387 | MIT 388 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | You can create a global config file at `~/.config/terminal-ai/config.json`, or a local config file at `./terminal-ai.json` in a folder where you want to run Terminal‑AI. `.toml` files are also supported. 4 | 5 | ## Example 6 | 7 | In fact this file is generated by the `gen-config-md` command defined in the [terminal-ai.toml](../terminal-ai.toml) for this project. 8 | 9 | ## Properties 10 | 11 | ### `default_model` (optional) 12 | 13 | - **Type:** `string` 14 | - **Description:** Specifies the default AI model to be used. 15 | 16 | ### `openai_api_key` (optional) 17 | 18 | - **Type:** `string` 19 | - **Description:** The API key for accessing OpenAI services. 20 | - **Default:** `process.env.OPENAI_API_KEY` 21 | 22 | ### `openai_api_url` (optional) 23 | 24 | - **Type:** `string` 25 | - **Description:** The URL for the OpenAI API. 26 | - **Default:** `process.env.OPENAI_API_URL` 27 | 28 | ### `gemini_api_key` (optional) 29 | 30 | - **Type:** `string` 31 | - **Description:** The API key for accessing Gemini services. 32 | - **Default:** `process.env.GEMINI_API_KEY` 33 | 34 | ### `gemini_api_url` (optional) 35 | 36 | - **Type:** `string` 37 | - **Description:** The URL for the Gemini (Google Gen AI) API. 38 | - **Default:** `process.env.GEMINI_API_URL` 39 | 40 | ### `anthropic_api_key` (optional) 41 | 42 | - **Type:** `string` 43 | - **Description:** The API key for accessing Anthropic services. 44 | - **Default:** `process.env.ANTHROPIC_API_KEY` 45 | 46 | ### `groq_api_key` (optional) 47 | 48 | - **Type:** `string` 49 | - **Description:** The API key for accessing Groq services. 50 | - **Default:** `process.env.GROQ_API_KEY` 51 | 52 | ### `groq_api_url` (optional) 53 | 54 | - **Type:** `string` 55 | - **Description:** The URL for the Groq API. 56 | - **Default:** `process.env.GROQ_API_URL` 57 | 58 | ### `ollama_host` (optional) 59 | 60 | - **Type:** `string` 61 | - **Description:** Host/URL of your local Ollama instance. 62 | - **Default:** `process.env.OLLAMA_HOST` 63 | 64 | ### `commands` (optional) 65 | 66 | - **Type:** `AICommand[]` 67 | - **Description:** A list of AI commands that can be executed. Each command is defined by the `AICommand` type. 68 | 69 | ## AICommand Type 70 | 71 | The `AICommand` type defines the structure of an AI command that can be executed. Below are the properties of the `AICommand` type. 72 | 73 | ### Properties 74 | 75 | #### `command` 76 | 77 | - **Type:** `string` 78 | - **Description:** The CLI command to be executed. 79 | 80 | #### `example` (optional) 81 | 82 | - **Type:** `string` 83 | - **Description:** An example to show in CLI help. 84 | 85 | #### `description` (optional) 86 | 87 | - **Type:** `string` 88 | - **Description:** A description of the command to be shown in CLI help. 89 | 90 | #### `variables` (optional) 91 | 92 | - **Type:** `Record` 93 | - **Description:** A record of variables that the command can use. Each variable is defined by the `AICommandVariable` type. 94 | 95 | #### `prompt` 96 | 97 | - **Type:** `string` 98 | - **Description:** The prompt to send to the AI model. 99 | 100 | #### `require_stdin` (optional) 101 | 102 | - **Type:** `boolean` 103 | - **Description:** Indicates whether the command requires piping output from another program to Terminal‑AI. 104 | 105 | ## AICommandVariable Type 106 | 107 | The `AICommandVariable` type defines the structure of a variable that can be used in an AI command. Below are the possible types of `AICommandVariable`. 108 | 109 | ### Types 110 | 111 | #### Shell Command 112 | 113 | - **Type:** `string` 114 | - **Description:** A shell command to run, the output of which will be used as the variable value. 115 | 116 | #### Input 117 | 118 | - **Type:** `{ type: "input"; message: string }` 119 | - **Description:** Gets text input from the user. 120 | - `type`: Must be `"input"`. 121 | - `message`: The message to show to the user. 122 | 123 | #### Select 124 | 125 | - **Type:** `{ type: "select"; message: string; choices: { value: string; title: string }[] }` 126 | - **Description:** Gets a choice from the user. 127 | - `type`: Must be `"select"`. 128 | - `message`: The message to show to the user. 129 | - `choices`: An array of choice objects, each conhoining: 130 | - `value`: The value of the choice. 131 | - `title`: The title of the choice to be displayed to the user. 132 | -------------------------------------------------------------------------------- /docs/npm-publishing.md: -------------------------------------------------------------------------------- 1 | # Publishing to npm 2 | 3 | This guide covers the process of publishing the Web3CLI package to npm under the `@web3ai` scope. 4 | 5 | ## Prerequisites 6 | 7 | 1. An npm account 8 | 2. Local npm login: `npm login` 9 | 3. Node.js 16 or newer installed 10 | 4. pnpm installed (`npm install -g pnpm`) 11 | 12 | ## Creating the Scope 13 | 14 | If the `@web3ai` scope doesn't exist or you don't have access to it, you need to create it: 15 | 16 | 1. Create a free organization on npm: 17 | - Go to https://www.npmjs.com/org/create 18 | - Enter "web3ai" as the organization name 19 | - Choose the Free plan 20 | - Complete the organization creation process 21 | 22 | 2. Or create the scope during first publish: 23 | - When you publish with `--access public`, npm will create the scope if it doesn't exist 24 | - You must be logged in with `npm login` 25 | 26 | ## Publishing Process 27 | 28 | ### Automated Publishing 29 | 30 | The easiest way to publish is using our scripts: 31 | 32 | #### On Windows: 33 | ``` 34 | scripts\publish-npm.bat 35 | ``` 36 | 37 | #### On macOS/Linux: 38 | ``` 39 | chmod +x scripts/publish-npm.sh 40 | ./scripts/publish-npm.sh 41 | ``` 42 | 43 | ### Manual Publishing 44 | 45 | If you prefer to publish manually: 46 | 47 | 1. Update the version in `package.json` 48 | ```bash 49 | # For patch releases (bug fixes) 50 | npm version patch 51 | 52 | # For minor releases (new features) 53 | npm version minor 54 | 55 | # For major releases (breaking changes) 56 | npm version major 57 | ``` 58 | 59 | 2. Build the package 60 | ```bash 61 | pnpm build 62 | ``` 63 | 64 | 3. Publish the package 65 | ```bash 66 | npm publish --access public 67 | ``` 68 | 69 | ## Scoped Package Explanation 70 | 71 | Web3CLI is published under the `@web3ai` scope as `@web3ai/cli`. Scoped packages provide several benefits: 72 | 73 | - Namespace protection: Ensures our package name is unique 74 | - Organization grouping: Allows grouping related packages together 75 | - Access control: Simplifies permission management for multiple packages 76 | 77 | ## Installation 78 | 79 | After publishing, users can install the package globally with: 80 | 81 | ```bash 82 | npm install -g @web3ai/cli 83 | ``` 84 | 85 | Or with pnpm: 86 | 87 | ```bash 88 | pnpm add -g @web3ai/cli 89 | ``` 90 | 91 | ## Versioning Guidelines 92 | 93 | We follow semantic versioning (semver): 94 | 95 | - **Patch** (1.0.x): Bug fixes and minor changes that don't affect APIs 96 | - **Minor** (1.x.0): New features in a backward-compatible manner 97 | - **Major** (x.0.0): Breaking changes that require user action to update 98 | 99 | Always document changes in CHANGELOG.md before publishing. 100 | 101 | ## Troubleshooting 102 | 103 | ### Common Issues 104 | 105 | #### "You need to authenticate" 106 | Run `npm login` before publishing. 107 | 108 | #### "You do not have permission to publish" 109 | Ensure you're a member of the @web3ai organization with publish rights. 110 | 111 | #### "Scope not found" 112 | The @web3ai scope doesn't exist. Create it first using the instructions in the "Creating the Scope" section. 113 | 114 | #### "Package name already exists" 115 | Someone else has already published a package with the same name. Double-check the package name in package.json. 116 | 117 | #### Build errors 118 | Run `pnpm build` manually to see detailed errors before publishing. -------------------------------------------------------------------------------- /docs/problem-solution.md: -------------------------------------------------------------------------------- 1 | # Web3CLI: Problem Statement and Solution Architecture 2 | 3 | ## The Problem: Challenges in Blockchain Development 4 | 5 | Blockchain and smart contract development present unique challenges that traditional software development tools don't adequately address: 6 | 7 | ### 1. Security-Critical Code Generation 8 | 9 | **Problem:** Smart contracts are immutable once deployed and directly handle financial assets. Security vulnerabilities can lead to catastrophic financial losses, yet many developers lack expertise in security best practices. 10 | 11 | **Statistics:** 12 | - Over $3.8 billion was lost to DeFi hacks and exploits in 2022 alone 13 | - Common vulnerabilities like reentrancy, integer overflow, and access control issues persist in production contracts 14 | - Security audits are expensive ($15,000-$80,000) and often inaccessible to smaller teams 15 | 16 | ### 2. Smart Contract Complexity 17 | 18 | **Problem:** Smart contracts implement complex financial, governance, and business logic in a constrained programming environment with unique execution models. 19 | 20 | **Challenges:** 21 | - The gas-based execution model requires specialized optimization knowledge 22 | - Understanding state transitions and transaction ordering effects is non-intuitive 23 | - Implementing standards correctly (ERC-20, ERC-721, etc.) has many edge cases 24 | 25 | ### 3. Documentation and Explainability Gap 26 | 27 | **Problem:** Smart contracts often lack clear documentation explaining their behavior, permissions, and security models. 28 | 29 | **Impact:** 30 | - Users interact with contracts without understanding risks 31 | - Developers build on top of existing contracts without full understanding 32 | - Auditors must spend excessive time reverse-engineering intent 33 | 34 | ## Why a CLI Solution? 35 | 36 | We chose to implement Web3CLI as a command-line interface for several key reasons: 37 | 38 | ### 1. Developer Workflow Integration 39 | 40 | **Advantage:** A CLI tool integrates smoothly into existing developer workflows for smart contract development. 41 | 42 | - Works alongside code editors, version control, and testing frameworks 43 | - Can be incorporated into CI/CD pipelines 44 | - Doesn't require switching context to a separate application 45 | 46 | ### 2. Scriptability and Automation 47 | 48 | **Advantage:** CLI tools can be easily scripted and automated. 49 | 50 | - Enables batch processing of multiple contracts 51 | - Allows for integration with build systems 52 | - Can be used in non-interactive environments (servers, containers) 53 | 54 | ### 3. Low Overhead and Accessibility 55 | 56 | **Advantage:** CLI tools have minimal resource requirements and wide compatibility. 57 | 58 | - Works across different operating systems 59 | - No complex installation or setup process 60 | - Doesn't require hosting infrastructure 61 | 62 | ### 4. Focus on Core Functionality 63 | 64 | **Advantage:** The CLI interface allows us to focus on core functionality rather than UI/UX concerns. 65 | 66 | - Faster development iterations for critical features 67 | - Lower maintenance burden 68 | - Emphasis on robust functionality over visual polish 69 | 70 | ## How Web3CLI Solves Each Problem 71 | 72 | ### 1. Secure Smart Contract Generation 73 | 74 | **Solution:** Web3CLI uses a multi-agent AI approach combined with security-focused vector search to generate secure contracts. 75 | 76 | - **Code Writer Agent** generates initial code with security in mind 77 | - **Security Audit Agent** identifies potential vulnerabilities 78 | - **Vector Search** retrieves relevant security patterns from a curated knowledge base 79 | - **Web Search** provides up-to-date security best practices 80 | - Detailed security considerations accompany each generated contract 81 | 82 | ### 2. Contract Explainability 83 | 84 | **Solution:** Web3CLI analyzes existing contracts to provide plain-English explanations of functionality, permissions, and security patterns. 85 | 86 | - Decomposes complex contracts into digestible sections 87 | - Identifies key functions and their purposes 88 | - Highlights permission structures and access controls 89 | - Flags potential security concerns 90 | - Documents interaction patterns with other contracts 91 | 92 | ### 3. Documentation Enhancement 93 | 94 | **Solution:** Web3CLI automatically generates comprehensive documentation for smart contracts. 95 | 96 | - Creates function-level documentation 97 | - Explains security considerations 98 | - Documents permission structures 99 | - Provides usage examples 100 | - Highlights potential integration considerations 101 | 102 | ### 4. Development Acceleration 103 | 104 | **Solution:** Web3CLI reduces development time by automating boilerplate code generation and providing quick access to relevant information. 105 | 106 | - Generates standard-compliant contracts from natural language 107 | - Creates test files to verify functionality 108 | - Provides immediate access to relevant documentation via vector search 109 | - Reduces research time through targeted web searches 110 | 111 | ## Core Technologies and Libraries 112 | 113 | Web3CLI leverages a specialized stack of technologies to deliver its capabilities: 114 | 115 | ### AI and Language Models 116 | 117 | - **OpenAI API** - Powers the core natural language understanding and code generation capabilities 118 | - **Claude API** - Used for the Security Audit agent for deep code analysis 119 | - **LangChain** - Framework for creating chains of LLM operations and managing the multi-agent system 120 | 121 | ### Vector Database and Knowledge Retrieval 122 | 123 | - **OpenAI Embeddings** - Converts text into vector representations for semantic search 124 | - **LangChain MemoryVectorStore** - In-memory vector database with persistence 125 | - **Cheerio** - HTML parsing for documentation extraction 126 | - **RecursiveCharacterTextSplitter** - Intelligent document chunking for storage and retrieval 127 | 128 | ### Blockchain Integration 129 | 130 | - **ethers.js** - Ethereum library for interacting with blockchain networks 131 | - **Solidity Compiler (solc-js)** - For validating generated contracts 132 | - **Hardhat** - Development environment integration for testing 133 | 134 | ### CLI and Infrastructure 135 | 136 | - **CAC (Command And Conquer)** - Lightweight CLI framework 137 | - **Chalk** - Terminal styling for better user experience 138 | - **Inquirer** - Interactive command prompts 139 | - **Node.js Filesystem API** - Local storage and file management 140 | - **TOML** - Configuration file format 141 | 142 | ## Unique Architectural Elements 143 | 144 | What makes Web3CLI special compared to other solutions: 145 | 146 | ### 1. Hierarchical Multi-Agent System 147 | 148 | Unlike simple LLM-based code generators, Web3CLI implements a team of specialized agents that collaborate with different areas of expertise: 149 | 150 | - Each agent is optimized for a specific task (code generation, security, style, etc.) 151 | - The Coordinator Agent orchestrates the workflow and ensures cohesion 152 | - Agents can use different underlying models optimized for their specific tasks 153 | 154 | ### 2. Local Vector Database with Security Focus 155 | 156 | Web3CLI maintains a local vector database of blockchain security patterns and documentation: 157 | 158 | - Curated security knowledge from trusted sources 159 | - Up-to-date information on best practices 160 | - Semantic search for finding relevant patterns 161 | - Persistent storage that doesn't require external services 162 | 163 | ### 3. Hybrid Web+Vector Search 164 | 165 | For comprehensive information retrieval, Web3CLI combines: 166 | 167 | - Local vector search for speed and reliability 168 | - Web search for up-to-date information 169 | - Results fusion for comprehensive context 170 | 171 | ### 4. Security-First Generation Workflow 172 | 173 | The entire generation pipeline is designed with security as the primary concern: 174 | 175 | 1. Security patterns are injected during initial code generation 176 | 2. Dedicated security audit phase identifies vulnerabilities 177 | 3. Explicit security considerations accompany all generated code 178 | 4. Security-focused linting and style improvements 179 | 180 | ## Conclusion 181 | 182 | Web3CLI addresses critical pain points in blockchain development through a specialized CLI tool that leverages AI, vector databases, and a security-first multi-agent architecture. By focusing on the developer workflow and emphasizing security, it significantly improves the smart contract creation process while reducing potential vulnerabilities. 183 | 184 | The tool represents a specialized solution to the unique challenges of blockchain development rather than simply applying general code generation techniques to the blockchain domain. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@web3ai/cli", 3 | "version": "1.1.7", 4 | "description": "Your AI-powered command-line companion for seamless Web3 development. Ask questions, get code suggestions, and accelerate your blockchain projects.", 5 | "type": "module", 6 | "bin": { 7 | "web3cli": "./dist/cli.js" 8 | }, 9 | "files": [ 10 | "dist", 11 | "schema.json", 12 | "scripts/create-default-config.js" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/shivatmax/web3cli" 17 | }, 18 | "homepage": "https://github.com/shivatmax/web3cli#readme", 19 | "bugs": { 20 | "url": "https://github.com/shivatmax/web3cli/issues" 21 | }, 22 | "author": "shiv-awasthi", 23 | "email": "awasthishiv0987@gmail.com", 24 | "keywords": [ 25 | "web3", 26 | "solidity", 27 | "smart-contracts", 28 | "cli", 29 | "web3cli", 30 | "web3ai", 31 | "llm", 32 | "ai", 33 | "openai", 34 | "help", 35 | "assistant", 36 | "blockchain", 37 | "smart contracts", 38 | "ethereum", 39 | "developer tools", 40 | "devtool", 41 | "gpt", 42 | "large language model", 43 | "code assistant", 44 | "copilot", 45 | "uups-proxy", 46 | "transparent-proxy" 47 | ], 48 | "license": "MIT", 49 | "scripts": { 50 | "prepare": "husky", 51 | "build": "npm run gen-config-schema && tsup", 52 | "postinstall": "node scripts/create-default-config.js", 53 | "dev": "npm run build -- --watch", 54 | "prepublishOnly": "npm run build", 55 | "gen-config-schema": "bun scripts/gen-config-schema.ts", 56 | "link": "npm link", 57 | "publish-package": "npm publish --access public" 58 | }, 59 | "dependencies": { 60 | "@ai-sdk/anthropic": "^1.0.0", 61 | "@ai-sdk/openai": "^1.0.0", 62 | "@anthropic-ai/sdk": "0.50.3", 63 | "@google/generative-ai": "^0.24.1", 64 | "@langchain/community": "^0.3.42", 65 | "@langchain/core": "^0.3.55", 66 | "@langchain/openai": "^0.5.10", 67 | "@mozilla/readability": "^0.6.0", 68 | "ai": "4.3.15", 69 | "axios": "^1.9.0", 70 | "cac": "^6.7.14", 71 | "cheerio": "^1.0.0", 72 | "cli-cursor": "^5.0.0", 73 | "colorette": "^2.0.20", 74 | "commander": "13.1.0", 75 | "ethers": "^6.14.0", 76 | "fast-glob": "^3.3.2", 77 | "joycon": "^3.1.1", 78 | "jsdom": "^26.1.0", 79 | "langchain": "^0.3.24", 80 | "log-update": "^6.0.0", 81 | "marked": "15.0.11", 82 | "marked-terminal": "7.3.0", 83 | "node-html-parser": "^7.0.1", 84 | "openai": "^4.97.0", 85 | "ora": "5.4.1", 86 | "prompts": "2.4.2", 87 | "smol-toml": "^1.3.1", 88 | "terminal-link": "^4.0.0", 89 | "tslib": "^2.6.2", 90 | "update-notifier": "7.3.1", 91 | "zod": "^3.22.0" 92 | }, 93 | "devDependencies": { 94 | "@types/jsdom": "^21.1.7", 95 | "@types/marked-terminal": "^6.1.1", 96 | "@types/node": "22.15.17", 97 | "@types/ora": "3.1.0", 98 | "@types/prompts": "^2.4.9", 99 | "@types/update-notifier": "^6.0.8", 100 | "husky": "^9.1.7", 101 | "rimraf": "6.0.1", 102 | "tsup": "8.4.0", 103 | "typescript": "5.8.3", 104 | "zod-to-json-schema": "3.24.5" 105 | }, 106 | "engines": { 107 | "node": ">=18.0.0", 108 | "npm": ">=9.0.0" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /scripts/create-default-config.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | try { 7 | // Determine this script's directory 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | // Example config is in the project root 12 | const exampleConfigPath = path.join(__dirname, '..', 'web3cli.example.toml'); 13 | const destPath = path.resolve(process.cwd(), 'web3cli.toml'); 14 | 15 | if (!fs.existsSync(destPath)) { 16 | fs.copyFileSync(exampleConfigPath, destPath); 17 | console.log('✨ Created default Web3CLI config: web3cli.toml'); 18 | } 19 | } catch (error) { 20 | console.error('⚠️ Could not create default config file:', error); 21 | } -------------------------------------------------------------------------------- /scripts/gen-config-schema.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import { ConfigSchema } from "../src/services/config/config.js" 3 | import { zodToJsonSchema } from "zod-to-json-schema" 4 | 5 | const jsonSchema = zodToJsonSchema(ConfigSchema, "Config") 6 | 7 | fs.writeFileSync("schema.json", JSON.stringify(jsonSchema, null, 2)) 8 | -------------------------------------------------------------------------------- /scripts/publish-npm.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: Script to publish the @web3ai/cli package to npm 3 | 4 | :: Change to project root directory 5 | cd %~dp0\.. 6 | 7 | :: Clean up previous builds 8 | if exist dist rmdir /s /q dist 9 | 10 | :: Install dependencies 11 | echo Installing dependencies... 12 | call pnpm install 13 | 14 | :: Run build process 15 | echo Building package... 16 | call pnpm build 17 | 18 | :: Publish package 19 | echo Publishing package to npm... 20 | call npm publish --access public 21 | 22 | echo Package published successfully! 23 | echo You can now install it with: npm install -g @web3ai/cli 24 | pause -------------------------------------------------------------------------------- /scripts/publish-npm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to publish the @web3ai/cli package to npm 3 | 4 | # Make sure we're in the project root 5 | cd "$(dirname "$0")/.." 6 | 7 | # Clean up previous builds 8 | rm -rf dist 9 | 10 | # Install dependencies (if needed) 11 | echo "Installing dependencies..." 12 | npm install 13 | 14 | # Run build process 15 | echo "Building package..." 16 | npm run build 17 | 18 | # Publish package 19 | echo "Publishing package to npm..." 20 | npm publish --access public 21 | 22 | echo "Package published successfully!" 23 | echo "You can now install it with: npm install -g @web3ai/cli" -------------------------------------------------------------------------------- /src/agents/code-writer.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "../services/ai/mastra-shim.js"; 2 | import { z } from "zod"; 3 | 4 | /** 5 | * CodeWriterAgent - Translates natural language requirements into Solidity code 6 | * 7 | * This agent is responsible for the initial generation of Solidity smart contracts 8 | * based on user requirements expressed in natural language. 9 | */ 10 | export class CodeWriterAgent { 11 | agent: Agent; 12 | 13 | constructor(model: string = "gpt-4o-mini") { 14 | this.agent = new Agent({ 15 | name: "CodeWriter", 16 | instructions: 17 | "You are an expert Solidity developer who writes clean, secure smart contracts." + 18 | "Your task is to translate natural language requirements into well-structured Solidity code." + 19 | "Focus on implementing the core functionality while following security best practices." + 20 | "Always use the latest Solidity version (^0.8.20) unless specified otherwise." + 21 | "Include comprehensive NatSpec comments." + 22 | "\n\n" + 23 | "IMPORTANT FORMATTING INSTRUCTIONS:\n" + 24 | "1. Start your response with '```solidity' and end with '```'\n" + 25 | "2. Include ONLY valid Solidity code within these markers\n" + 26 | "3. Always begin with SPDX license identifier and pragma statement\n" + 27 | "4. Write complete, compilable contracts\n" + 28 | "5. DO NOT include explanations before or after the code block\n" + 29 | "6. Make sure code follows best security practices\n" + 30 | "7. If an upgradeable contract is requested, use OpenZeppelin's upgradeable contracts library properly", 31 | model: model, 32 | }); 33 | } 34 | 35 | /** 36 | * Generate Solidity code based on the given requirements 37 | * 38 | * @param prompt Natural language description of the contract requirements 39 | * @param context Additional context (optional) 40 | * @returns The generated Solidity code 41 | */ 42 | async generateCode(prompt: string, context?: string): Promise { 43 | console.log("Generating initial smart contract code..."); 44 | console.log(`[CodeWriter] Generating code for: ${prompt}`); 45 | 46 | try { 47 | // Prepare input for the agent 48 | const input = { 49 | prompt: prompt, 50 | context: context || "", 51 | format: "solidity", 52 | requirements: [ 53 | "Create complete, compilable Solidity code", 54 | "Include proper SPDX license and pragma statement", 55 | "Implement security best practices", 56 | "Generate NatSpec documentation", 57 | "Return ONLY code within ```solidity code blocks", 58 | ] 59 | }; 60 | 61 | // Call the agent with the input 62 | const response = await this.agent.run(input); 63 | 64 | // Extract the code from the response 65 | let output = response.output || ""; 66 | 67 | // Ensure proper code block formatting if not already present 68 | if (!output.trim().startsWith("```solidity") && !output.trim().startsWith("```")) { 69 | output = "```solidity\n" + output + "\n```"; 70 | } 71 | 72 | return output; 73 | } catch (error) { 74 | console.error("Error generating code:", error); 75 | // Fall back to a simple template in case of error 76 | return `// SPDX-License-Identifier: MIT 77 | pragma solidity ^0.8.20; 78 | 79 | /** 80 | * @title Contract based on: ${prompt} 81 | * @notice Error occurred during generation 82 | */ 83 | contract GeneratedContract { 84 | // Error occurred during generation 85 | }`; 86 | } 87 | } 88 | } 89 | 90 | export const codeWriterSchema = { 91 | inputSchema: z.object({ 92 | prompt: z.string().describe("Contract requirements"), 93 | context: z.string().optional().describe("Additional context"), 94 | }), 95 | outputSchema: z.object({ 96 | code: z.string().describe("Generated Solidity code"), 97 | }) 98 | }; -------------------------------------------------------------------------------- /src/agents/coordinator.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "../services/ai/mastra-shim.js"; 2 | import { z } from "zod"; 3 | import { CodeWriterAgent } from "./code-writer.js"; 4 | import { SecurityAuditAgent } from "./security-audit.js"; 5 | import { LintingAgent } from "./linting.js"; 6 | import { FunctionalityAgent } from "./functionality.js"; 7 | import { WebSearchAgent } from "./web-search.js"; 8 | import { VectorStoreAgent } from "./vector-store.js"; 9 | import fs from "fs"; 10 | import path from "path"; 11 | 12 | /** 13 | * Ensures a directory exists, creating it and any parent directories if needed 14 | * @param dirPath Directory path to ensure exists 15 | */ 16 | function ensureDirectoryExists(dirPath: string): void { 17 | try { 18 | fs.mkdirSync(dirPath, { recursive: true }); 19 | } catch (error: any) { 20 | if (error.code !== 'EEXIST') { 21 | throw error; 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * Safely write file content to a path, ensuring directory exists 28 | * @param filePath Path to write to 29 | * @param content Content to write 30 | * @returns true if successful 31 | */ 32 | function safeWriteFileSync(filePath: string, content: string): boolean { 33 | try { 34 | const dir = path.dirname(filePath); 35 | ensureDirectoryExists(dir); 36 | fs.writeFileSync(filePath, content); 37 | return true; 38 | } catch (error) { 39 | console.error(`❌ Error writing to ${filePath}:`, error); 40 | return false; 41 | } 42 | } 43 | 44 | /** 45 | * CoordinatorAgent - Orchestrates the smart contract generation process 46 | * 47 | * This agent is responsible for coordinating the workflow between all specialized 48 | * agents to produce high-quality smart contracts from natural language requirements. 49 | */ 50 | export class CoordinatorAgent { 51 | private agent: Agent; 52 | private codeWriter: CodeWriterAgent; 53 | private securityAuditor: SecurityAuditAgent; 54 | private lintingAgent: LintingAgent; 55 | private functionalityChecker: FunctionalityAgent; 56 | private webSearchAgent: WebSearchAgent; 57 | private vectorStoreAgent: VectorStoreAgent; 58 | 59 | constructor(models: { 60 | coordinator?: string; 61 | codeWriter?: string; 62 | securityAuditor?: string; 63 | lintingAgent?: string; 64 | functionalityChecker?: string; 65 | webSearchAgent?: string; 66 | vectorStoreAgent?: string; 67 | } = {}) { 68 | this.agent = new Agent({ 69 | name: "CoordinatorAgent", 70 | instructions: 71 | "You are the coordinator agent that orchestrates the smart contract generation process." + 72 | "You will follow this workflow:\n" + 73 | "1. Use WebSearchAgent to gather relevant information if needed\n" + 74 | "2. Use VectorStoreAgent to find relevant security patterns and examples\n" + 75 | "3. Use CodeWriter to generate initial Solidity code\n" + 76 | "4. Use SecurityAuditor to check for security issues\n" + 77 | "5. Use LintingAgent to improve code style and quality\n" + 78 | "6. Use FunctionalityChecker to verify the contract works as intended and generate tests if requested\n" + 79 | "Return the final contract with all improvements and security enhancements applied.", 80 | model: models.coordinator || "gpt-4o-mini", 81 | }); 82 | 83 | this.codeWriter = new CodeWriterAgent(models.codeWriter); 84 | this.securityAuditor = new SecurityAuditAgent(models.securityAuditor); 85 | this.lintingAgent = new LintingAgent(models.lintingAgent); 86 | this.functionalityChecker = new FunctionalityAgent(models.functionalityChecker); 87 | this.webSearchAgent = new WebSearchAgent(models.webSearchAgent); 88 | this.vectorStoreAgent = new VectorStoreAgent(models.vectorStoreAgent); 89 | } 90 | 91 | /** 92 | * Generate a smart contract using the multi-agent system 93 | * 94 | * @param prompt Contract requirements in natural language 95 | * @param options Generation options 96 | * @returns Generated contract, security notes, and optional tests 97 | */ 98 | async generateContract( 99 | prompt: string, 100 | options: { 101 | search?: boolean; 102 | readDocs?: string; 103 | hardhat?: boolean; 104 | output?: string; 105 | } = {} 106 | ): Promise<{ 107 | code: string; 108 | securityNotes: string; 109 | testCode?: string; 110 | }> { 111 | console.log("🚀 Coordinator Agent starting workflow for contract generation..."); 112 | console.log("[CoordinatorAgent] Planning contract generation workflow"); 113 | console.log("────────────────────────────────────────────"); 114 | 115 | // Step 1: Web search (optional) 116 | let webSearchResults = ""; 117 | if (options.search) { 118 | console.log("Running web search..."); 119 | webSearchResults = await this.webSearchAgent.searchWeb( 120 | `solidity ${prompt} implementation examples` 121 | ); 122 | console.log("✓ Web search completed"); 123 | } 124 | 125 | // Step 2: Vector search for patterns 126 | let securityPatterns = ""; 127 | if (options.readDocs) { 128 | console.log("Searching vector database for security patterns..."); 129 | const patternSearchQuery = prompt.toLowerCase().includes("erc20") 130 | ? "erc20 allowlist security" 131 | : prompt.toLowerCase().includes("nft") 132 | ? "nft security patterns" 133 | : "solidity security best practices"; 134 | 135 | securityPatterns = await this.vectorStoreAgent.searchVectorStore( 136 | patternSearchQuery, 137 | options.readDocs || "security-patterns", 138 | 3 139 | ); 140 | console.log("✓ Vector search completed"); 141 | } 142 | 143 | // Step 3: Code generation 144 | console.log("Generating initial smart contract code..."); 145 | let context = webSearchResults + "\n\n" + securityPatterns; 146 | let initialCode = await this.codeWriter.generateCode(prompt, context); 147 | console.log("✓ Initial code generation completed"); 148 | 149 | // Step 4: Security audit 150 | console.log("Performing security audit..."); 151 | const auditResult = await this.securityAuditor.auditContract(initialCode, prompt); 152 | const securityIssues = auditResult.issues; 153 | let secureCode = auditResult.fixedCode; 154 | console.log("✓ Security audit completed"); 155 | 156 | // Step 5: Linting and style improvements 157 | console.log("Improving code style and quality..."); 158 | const lintResult = await this.lintingAgent.lintContract(secureCode); 159 | let lintedCode = lintResult.improvedCode; 160 | console.log("✓ Code style improvements completed"); 161 | 162 | // Step 6: Functionality check and testing 163 | console.log("Verifying functionality and generating tests..."); 164 | const functionalityResult = await this.functionalityChecker.verifyFunctionality( 165 | lintedCode, 166 | prompt, 167 | options.hardhat 168 | ); 169 | const finalCode = functionalityResult.improvedCode; 170 | const testCode = functionalityResult.testCode; 171 | console.log("✓ Functionality verification completed"); 172 | 173 | // Final console output 174 | console.log("────────────────────────────────────────────"); 175 | console.log("\n✅ Agent Mode process completed successfully!"); 176 | 177 | // Write the final output to file if specified 178 | if (finalCode && options.output) { 179 | try { 180 | // Save contract file 181 | if (safeWriteFileSync(options.output, finalCode)) { 182 | console.log(`\n✅ Contract saved to ${options.output}`); 183 | } 184 | 185 | // If hardhat tests were generated, save the test file too 186 | if (options.hardhat && testCode && options.output) { 187 | const contractName = options.output.replace(/\.sol$/, ''); 188 | const testFilename = `${contractName}.test.js`; 189 | if (safeWriteFileSync(testFilename, testCode)) { 190 | console.log(`✅ Test file saved to ${testFilename}`); 191 | } 192 | } 193 | } catch (error) { 194 | console.error(`❌ Error saving files:`, error); 195 | } 196 | } 197 | 198 | return { 199 | code: finalCode || "No code generated", 200 | securityNotes: securityIssues || "No security notes provided", 201 | testCode: testCode, 202 | }; 203 | } 204 | } 205 | 206 | export const coordinatorSchema = { 207 | inputSchema: z.object({ 208 | prompt: z.string().describe("Contract requirements"), 209 | options: z.object({ 210 | search: z.boolean().optional().describe("Whether to perform web search"), 211 | readDocs: z.string().optional().describe("Vector DB collection to search"), 212 | hardhat: z.boolean().optional().describe("Whether to generate Hardhat tests"), 213 | output: z.string().optional().describe("Output file path"), 214 | }).optional(), 215 | }), 216 | outputSchema: z.object({ 217 | code: z.string().describe("Generated Solidity code"), 218 | securityNotes: z.string().describe("Security considerations and notes"), 219 | testCode: z.string().optional().describe("Test code if requested"), 220 | }) 221 | }; -------------------------------------------------------------------------------- /src/agents/functionality.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "../services/ai/mastra-shim.js"; 2 | import { z } from "zod"; 3 | 4 | /** 5 | * FunctionalityAgent - Verifies contract functionality against requirements 6 | * 7 | * This agent is responsible for checking that the generated smart contract 8 | * implements all the required functionality correctly and can generate tests. 9 | */ 10 | export class FunctionalityAgent { 11 | private agent: Agent; 12 | 13 | constructor(model: string = "gpt-4o-mini") { 14 | this.agent = new Agent({ 15 | name: "FunctionalityChecker", 16 | instructions: 17 | "You are an expert in testing and verifying Solidity smart contracts." + 18 | "Analyze the provided code to ensure it meets the specified requirements." + 19 | "Check for edge cases and potential logical errors." + 20 | "If requested, create appropriate test cases using Hardhat." + 21 | "Suggest improvements to enhance the contract's functionality.", 22 | model: model, 23 | }); 24 | } 25 | 26 | /** 27 | * Verify contract functionality against requirements 28 | * 29 | * @param code The Solidity code to check 30 | * @param requirements Original contract requirements 31 | * @param generateTests Whether to generate test cases 32 | * @returns Verification results with feedback, improved code, and optional tests 33 | */ 34 | async verifyFunctionality( 35 | code: string, 36 | requirements: string, 37 | generateTests: boolean = false 38 | ): Promise<{ 39 | feedback: string; 40 | improvedCode: string; 41 | testCode?: string; 42 | }> { 43 | console.log("Verifying functionality and generating tests..."); 44 | console.log("[FunctionalityChecker] Verifying implementation against requirements"); 45 | 46 | try { 47 | // Prepare input for the agent 48 | const input = { 49 | code: code, 50 | requirements: requirements, 51 | generateTests: generateTests 52 | }; 53 | 54 | // Call the agent with the input 55 | const response = await this.agent.run(input); 56 | 57 | // Extract the feedback, improved code, and test code from the response 58 | return { 59 | feedback: response.feedback || "No functionality feedback provided.", 60 | improvedCode: response.improvedCode || code, 61 | testCode: generateTests ? response.testCode : undefined 62 | }; 63 | } catch (error) { 64 | console.error("Error verifying functionality:", error); 65 | 66 | // Extract the contract name for error test generation if needed 67 | const contractNameMatch = code.match(/contract\s+(\w+)/); 68 | const contractName = contractNameMatch ? contractNameMatch[1] : "Contract"; 69 | 70 | // Return a fallback response in case of error 71 | return { 72 | feedback: "Error occurred during functionality verification.", 73 | improvedCode: code, 74 | testCode: generateTests ? 75 | `// Error generating tests for ${contractName}\n` + 76 | `// Please manually create tests based on the contract requirements.` : 77 | undefined 78 | }; 79 | } 80 | } 81 | } 82 | 83 | export const functionalitySchema = { 84 | inputSchema: z.object({ 85 | code: z.string().describe("Solidity code to check"), 86 | requirements: z.string().describe("Original requirements"), 87 | generateTests: z.boolean().optional().describe("Whether to generate tests"), 88 | }), 89 | outputSchema: z.object({ 90 | feedback: z.string().describe("Functionality feedback"), 91 | improvedCode: z.string().describe("Code with functional improvements"), 92 | testCode: z.string().optional().describe("Test code if requested"), 93 | }) 94 | }; -------------------------------------------------------------------------------- /src/agents/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Agent System for Web3CLI 3 | * 4 | * This module exports the multi-agent system that collaborates to generate 5 | * secure and high-quality smart contracts from natural language descriptions. 6 | */ 7 | 8 | // Agent implementations 9 | export * from './code-writer.js'; 10 | export * from './security-audit.js'; 11 | export * from './linting.js'; 12 | export * from './functionality.js'; 13 | export * from './coordinator.js'; 14 | export * from './web-search.js'; 15 | export * from './vector-store.js'; 16 | 17 | // Re-export agent coordinator for simplified imports 18 | // export { CoordinatorAgent } from './coordinator'; -------------------------------------------------------------------------------- /src/agents/linting.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "../services/ai/mastra-shim.js"; 2 | import { z } from "zod"; 3 | 4 | /** 5 | * LintingAgent - Improves code style and quality of Solidity contracts 6 | * 7 | * This agent is responsible for checking code style, consistency, and 8 | * adherence to best practices in Solidity smart contracts. 9 | */ 10 | export class LintingAgent { 11 | private agent: Agent; 12 | 13 | constructor(model: string = "gpt-4o-mini") { 14 | this.agent = new Agent({ 15 | name: "LintingAgent", 16 | instructions: 17 | "You are a Solidity code style and quality expert." + 18 | "Review the code for style issues, consistency, and adherence to best practices." + 19 | "Check for:\n" + 20 | "- Proper naming conventions\n" + 21 | "- Code organization\n" + 22 | "- Gas optimizations\n" + 23 | "- Documentation completeness\n" + 24 | "Provide specific recommendations on how to improve the code quality.", 25 | model: model, 26 | }); 27 | } 28 | 29 | /** 30 | * Lint a Solidity contract for style and quality issues 31 | * 32 | * @param code The Solidity code to lint 33 | * @returns Linting results with issues and improved code 34 | */ 35 | async lintContract(code: string): Promise<{ 36 | issues: string; 37 | improvedCode: string; 38 | }> { 39 | console.log("Improving code style and quality..."); 40 | console.log("[LintingAgent] Reviewing code style and quality"); 41 | 42 | try { 43 | // Prepare input for the agent 44 | const input = { 45 | code: code 46 | }; 47 | 48 | // Call the agent with the input 49 | const response = await this.agent.run(input); 50 | 51 | // Extract the issues and improved code from the response 52 | const issues = response.issues || "No style issues found."; 53 | const improvedCode = response.improvedCode || code; 54 | 55 | return { 56 | issues, 57 | improvedCode 58 | }; 59 | } catch (error) { 60 | console.error("Error linting contract:", error); 61 | 62 | // Return a fallback response in case of error 63 | return { 64 | issues: "Error occurred during linting.", 65 | improvedCode: code 66 | }; 67 | } 68 | } 69 | } 70 | 71 | export const lintingSchema = { 72 | inputSchema: z.object({ 73 | code: z.string().describe("Solidity code to lint"), 74 | }), 75 | outputSchema: z.object({ 76 | issues: z.string().describe("Style and quality issues"), 77 | improvedCode: z.string().describe("Improved code with better style"), 78 | }) 79 | }; -------------------------------------------------------------------------------- /src/agents/security-audit.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "../services/ai/mastra-shim.js"; 2 | import { z } from "zod"; 3 | 4 | /** 5 | * SecurityAuditAgent - Analyzes Solidity code for security vulnerabilities 6 | * 7 | * This agent is responsible for checking generated smart contracts for 8 | * potential security issues and suggesting improvements. 9 | */ 10 | export class SecurityAuditAgent { 11 | private agent: Agent; 12 | 13 | constructor(model: string = "gpt-4o-mini") { 14 | this.agent = new Agent({ 15 | name: "SecurityAuditor", 16 | instructions: 17 | "You are a security auditor specializing in Solidity smart contracts." + 18 | "Analyze the provided code for security vulnerabilities including but not limited to:" + 19 | "- Reentrancy attacks\n" + 20 | "- Integer overflow/underflow\n" + 21 | "- Access control issues\n" + 22 | "- Logic errors\n" + 23 | "- Gas optimization issues\n" + 24 | "Provide detailed feedback on security issues and suggest specific code changes to fix them." + 25 | "Format your response as a list of issues with severity levels (Critical, High, Medium, Low) and recommended fixes.", 26 | model: model, 27 | }); 28 | } 29 | 30 | /** 31 | * Audit a smart contract for security vulnerabilities 32 | * 33 | * @param code The Solidity code to audit 34 | * @param requirements Original contract requirements 35 | * @returns Audit results with issues and fixed code 36 | */ 37 | async auditContract(code: string, requirements: string): Promise<{ 38 | issues: string; 39 | fixedCode: string; 40 | }> { 41 | console.log("Performing security audit..."); 42 | console.log("[SecurityAuditor] Analyzing code for security vulnerabilities"); 43 | 44 | try { 45 | // Prepare input for the agent 46 | const input = { 47 | code: code, 48 | requirements: requirements 49 | }; 50 | 51 | // Call the agent with the input 52 | const response = await this.agent.run(input); 53 | 54 | // Extract the security issues and fixed code from the response 55 | const issues = response.issues || "No security issues found."; 56 | const fixedCode = response.fixedCode || code; 57 | 58 | return { 59 | issues, 60 | fixedCode 61 | }; 62 | } catch (error) { 63 | console.error("Error performing security audit:", error); 64 | 65 | // Return a fallback response in case of error 66 | return { 67 | issues: "Error occurred during security audit.", 68 | fixedCode: code 69 | }; 70 | } 71 | } 72 | } 73 | 74 | export const securityAuditSchema = { 75 | inputSchema: z.object({ 76 | code: z.string().describe("Solidity code to audit"), 77 | requirements: z.string().describe("Original requirements"), 78 | }), 79 | outputSchema: z.object({ 80 | issues: z.string().describe("Security issues found"), 81 | fixedCode: z.string().describe("Fixed code with security improvements"), 82 | }) 83 | }; -------------------------------------------------------------------------------- /src/agents/vector-store.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "../services/ai/mastra-shim.js"; 2 | import { z } from "zod"; 3 | import { VectorDB } from "../services/vector-db/vector-db.js"; 4 | 5 | /** 6 | * VectorStoreAgent - Retrieves relevant information from vector storage 7 | * 8 | * This agent is responsible for querying the vector database to find 9 | * security patterns, code examples, and best practices for smart contracts. 10 | */ 11 | export class VectorStoreAgent { 12 | private agent: Agent; 13 | private vectorDB: VectorDB; 14 | 15 | constructor(model: string = "gpt-4o-mini") { 16 | this.agent = new Agent({ 17 | name: "VectorStoreAgent", 18 | instructions: 19 | "You are a knowledge retrieval specialist for Solidity development." + 20 | "Create search queries for the vector database to find relevant:" + 21 | "- Security patterns\n" + 22 | "- Code examples\n" + 23 | "- Best practices\n" + 24 | "Your task is to formulate effective queries that will return the most helpful information.", 25 | model: model, 26 | }); 27 | 28 | this.vectorDB = new VectorDB(); 29 | } 30 | 31 | /** 32 | * Search the vector database for relevant information 33 | * 34 | * @param query The search query 35 | * @param collection The vector database collection name 36 | * @param limit Maximum number of results to return 37 | * @returns Search results as text 38 | */ 39 | async searchVectorStore( 40 | query: string, 41 | collection: string = "security-patterns", 42 | limit: number = 5 43 | ): Promise { 44 | console.log(`Searching vector database for ${collection}...`); 45 | console.log(`[VectorStoreAgent] Finding relevant ${collection}`); 46 | 47 | try { 48 | // First, use the agent to optimize the search query 49 | const input = { 50 | originalQuery: query, 51 | collection: collection, 52 | context: `Need to find information about ${query} in the ${collection} collection` 53 | }; 54 | 55 | // Call the agent to get optimized search queries 56 | const agentResponse = await this.agent.run(input); 57 | 58 | // Get optimized queries from agent or use original query 59 | const optimizedQueries = agentResponse.queries || [query]; 60 | 61 | // Collect results from all queries 62 | let allResults = []; 63 | for (const optimizedQuery of Array.isArray(optimizedQueries) ? optimizedQueries : [optimizedQueries]) { 64 | const docs = await this.vectorDB.similaritySearch(collection, optimizedQuery, Math.ceil(limit / 2)); 65 | allResults.push(...docs); 66 | } 67 | 68 | // Remove duplicates (if any) 69 | const uniqueResults = this.removeDuplicateResults(allResults); 70 | 71 | // Limit to requested number 72 | const finalResults = uniqueResults.slice(0, limit); 73 | 74 | console.log("✓ Vector search completed"); 75 | 76 | if (finalResults.length > 0) { 77 | console.log(`[VectorStoreAgent] Found ${finalResults.length} relevant entries`); 78 | 79 | // Have the agent format and summarize the results if we have many 80 | if (finalResults.length > 3) { 81 | const formattingInput = { 82 | results: finalResults.map(d => d.pageContent), 83 | originalQuery: query 84 | }; 85 | 86 | const formattingResponse = await this.agent.run(formattingInput); 87 | return formattingResponse.summary || finalResults.map(d => d.pageContent).join("\n\n"); 88 | } 89 | 90 | return finalResults.map(d => d.pageContent).join("\n\n"); 91 | } else { 92 | console.log("[VectorStoreAgent] No relevant entries found"); 93 | return "No relevant information found in vector store."; 94 | } 95 | } catch (error) { 96 | console.error("❌ Vector search error:", error); 97 | return "Error searching vector store."; 98 | } 99 | } 100 | 101 | /** 102 | * Remove duplicate results based on content similarity 103 | * 104 | * @param results The search results to deduplicate 105 | * @returns Deduplicated results 106 | */ 107 | private removeDuplicateResults(results: any[]): any[] { 108 | const seen = new Set(); 109 | return results.filter(doc => { 110 | // Create a signature of the document content (first 50 chars) 111 | const contentSignature = doc.pageContent?.substring(0, 50); 112 | if (!contentSignature || seen.has(contentSignature)) { 113 | return false; 114 | } 115 | seen.add(contentSignature); 116 | return true; 117 | }); 118 | } 119 | } 120 | 121 | export const vectorStoreSchema = { 122 | inputSchema: z.object({ 123 | query: z.string().describe("The search query"), 124 | collection: z.string().optional().describe("Vector DB collection name"), 125 | limit: z.number().optional().describe("Maximum number of results"), 126 | }), 127 | outputSchema: z.object({ 128 | results: z.string().describe("Vector search results"), 129 | }) 130 | }; -------------------------------------------------------------------------------- /src/agents/web-search.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "../services/ai/mastra-shim.js"; 2 | import { z } from "zod"; 3 | import { getSearchResults } from "../services/search/search.js"; 4 | 5 | /** 6 | * WebSearchAgent - Searches the web for relevant information 7 | * 8 | * This agent is responsible for gathering relevant information from the web 9 | * to enhance the context for smart contract generation. 10 | */ 11 | export class WebSearchAgent { 12 | private agent: Agent; 13 | 14 | constructor(model: string = "gpt-4o-mini") { 15 | this.agent = new Agent({ 16 | name: "WebSearchAgent", 17 | instructions: 18 | "You are a web research specialist for Solidity development." + 19 | "Based on contract requirements, generate relevant search queries to find information about:" + 20 | "- Similar contract implementations\n" + 21 | "- Security best practices\n" + 22 | "- Design patterns\n" + 23 | "- Recent vulnerabilities or exploits\n" + 24 | "- Specific standards (ERC721, ERC1155, ERC2981, etc.)\n" + 25 | "- Gas optimization techniques\n" + 26 | "\n" + 27 | "Generate 3-5 specific and diverse search queries that will cover different aspects of the requirements.\n" + 28 | "For NFT collections, include searches for royalty implementations, metadata standards, minting patterns, and security considerations.\n" + 29 | "Always include at least one search query specifically about security best practices for the contract type.", 30 | model: model, 31 | }); 32 | } 33 | 34 | /** 35 | * Search the web for relevant information 36 | * 37 | * @param query The search query 38 | * @returns Search results as text 39 | */ 40 | async searchWeb(query: string): Promise { 41 | console.log("🔎 Searching the web for information..."); 42 | console.log(`[WebSearchAgent] Searching for information about: ${query}`); 43 | 44 | try { 45 | // First, generate optimized search queries using the agent 46 | const input = { 47 | query: query, 48 | topic: "smart contract", 49 | requireSearchTerms: true, 50 | domainHints: [ 51 | "NFTs", 52 | "royalties", 53 | "Solidity", 54 | "OpenZeppelin", 55 | "ERC721", 56 | "ERC2981" 57 | ] 58 | }; 59 | 60 | // Call the agent to get optimal search terms 61 | const agentResponse = await this.agent.run(input); 62 | 63 | // Get search terms from the agent or use default searches if agent fails 64 | let searchTerms = []; 65 | 66 | // Try different ways the agent might return search terms 67 | if (Array.isArray(agentResponse.searchTerms)) { 68 | searchTerms = agentResponse.searchTerms; 69 | } else if (typeof agentResponse.searchTerms === 'string') { 70 | searchTerms = agentResponse.searchTerms.split('\n').filter((term: string) => term.trim().length > 0); 71 | } else if (Array.isArray(agentResponse.queries)) { 72 | searchTerms = agentResponse.queries; 73 | } else if (typeof agentResponse.queries === 'string') { 74 | searchTerms = agentResponse.queries.split('\n').filter((term: string) => term.trim().length > 0); 75 | } else if (typeof agentResponse.output === 'string') { 76 | // Try to parse a list from the output 77 | const queryMatches = agentResponse.output.match(/["'](.+?)["']/g); 78 | if (queryMatches && queryMatches.length > 0) { 79 | searchTerms = queryMatches.map((m: string) => m.replace(/["']/g, '')); 80 | } else { 81 | searchTerms = agentResponse.output.split('\n') 82 | .filter((line: string) => line.trim().length > 10) 83 | .slice(0, 5); 84 | } 85 | } 86 | 87 | // Use default search terms if we couldn't get any from the agent 88 | if (!searchTerms || searchTerms.length === 0) { 89 | if (query.toLowerCase().includes('nft') && query.toLowerCase().includes('royalt')) { 90 | searchTerms = [ 91 | "solidity ERC721 NFT collection with royalties example code", 92 | "ERC2981 royalties implementation OpenZeppelin", 93 | "NFT collection security best practices Solidity", 94 | "gas efficient NFT minting patterns Solidity", 95 | "NFT metadata standards and best practices" 96 | ]; 97 | } else { 98 | // Generic fallback based on query 99 | searchTerms = [ 100 | `solidity ${query} implementation example`, 101 | `${query} security best practices blockchain`, 102 | `${query} gas optimization Ethereum`, 103 | `OpenZeppelin ${query} implementation` 104 | ]; 105 | } 106 | } 107 | 108 | console.log(`[WebSearchAgent] Generated search queries: ${searchTerms.join(', ')}`); 109 | 110 | // Perform searches for each term 111 | let allResults = ""; 112 | let searchCount = 0; 113 | 114 | // Limit to max 5 search terms to avoid excessive searches 115 | const searchTermsToUse = searchTerms.slice(0, 5); 116 | 117 | for (const term of searchTermsToUse) { 118 | console.log(`[WebSearchAgent] Searching for: "${term}"`); 119 | const results = await getSearchResults(term); 120 | if (results && results.trim().length > 0) { 121 | allResults += `\n\n--- SEARCH RESULTS: "${term}" ---\n${results}`; 122 | searchCount++; 123 | } 124 | } 125 | 126 | console.log(`✓ Web search completed - ${searchCount} searches performed`); 127 | 128 | // Use the agent to summarize the search results if they're extensive 129 | if (allResults.length > 3000) { 130 | console.log(`[WebSearchAgent] Summarizing search results...`); 131 | const summaryInput = { 132 | searchResults: allResults, 133 | originalQuery: query, 134 | task: "summarize", 135 | format: "concise" 136 | }; 137 | 138 | const summaryResponse = await this.agent.run(summaryInput); 139 | const summary = summaryResponse.summary || summaryResponse.output; 140 | 141 | if (summary && typeof summary === 'string' && summary.length > 100) { 142 | console.log(`[WebSearchAgent] Generated summary of search results`); 143 | return `SEARCH SUMMARY:\n${summary}\n\nFULL SEARCH RESULTS:\n${allResults}`; 144 | } 145 | } 146 | 147 | return allResults || "No relevant information found."; 148 | } catch (error) { 149 | console.error("❌ Web search error:", error); 150 | return `Error performing web search: ${error}. 151 | 152 | General information about NFT collections with royalties: 153 | - ERC-721 is the standard for non-fungible tokens on Ethereum 154 | - ERC-2981 is the royalty standard that allows marketplaces to identify royalty payments 155 | - OpenZeppelin provides secure implementations of these standards 156 | - Consider using ERC721Enumerable for collections that need on-chain enumeration 157 | - Security best practices include access control, pausability, and input validation 158 | - For royalties, implement the ERC2981 interface with royaltyInfo function`; 159 | } 160 | } 161 | } 162 | 163 | export const webSearchSchema = { 164 | inputSchema: z.object({ 165 | query: z.string().describe("The search query"), 166 | }), 167 | outputSchema: z.object({ 168 | results: z.string().describe("Search results"), 169 | }) 170 | }; -------------------------------------------------------------------------------- /src/cli/commands/builtin-commands.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in Commands 3 | * 4 | * This module provides built-in commands for the CLI. 5 | */ 6 | import { AICommand } from "../../services/config/config.js"; 7 | 8 | /** 9 | * Get built-in commands 10 | * 11 | * @returns Array of built-in commands 12 | */ 13 | export function getBuiltinCommands(): AICommand[] { 14 | return [ 15 | // Example built-in command 16 | { 17 | command: "explain-solidity", 18 | prompt: "Explain the following Solidity code: {code}", 19 | variables: { 20 | code: { 21 | type: "input", 22 | message: "Enter Solidity code to explain", 23 | }, 24 | }, 25 | }, 26 | ] 27 | } -------------------------------------------------------------------------------- /src/cli/commands/contract.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract Command 3 | * 4 | * This module implements the 'contract' command for working with 5 | * smart contracts. 6 | */ 7 | 8 | import { 9 | explainContract, 10 | auditContract, 11 | customContractRequest 12 | } from '../../services/contract/contract-commands.js'; 13 | import fs from 'fs'; 14 | import path from 'path'; 15 | 16 | /** 17 | * Register the contract command with the CLI 18 | * 19 | * @param cli The CLI command instance 20 | * @returns The configured command 21 | */ 22 | export function registerContractCommand(cli: any): void { 23 | // Ensure output directory exists 24 | fs.mkdirSync(path.join(process.cwd(), 'output'), { recursive: true }); 25 | 26 | // Main contract command 27 | cli 28 | .command('contract ', 'Analyze a smart contract') 29 | .option('-m, --model [model]', 'Choose the AI model to use, omit value to select interactively') 30 | .option('--network ', 'Ethereum network (default: sepolia)', { default: 'sepolia' }) 31 | .option('-o, --output [dir]', 'Output directory for results, omit value to use default') 32 | .option('--no-stream', 'Disable streaming output') 33 | .option('--read-docs ', 'Read indexed docs collection as context') 34 | .action(async (source: string, flags: any) => { 35 | // By default, the main contract command uses explain 36 | await explainContract(source, { 37 | model: flags.model, 38 | network: flags.network, 39 | output: flags.output !== false ? (flags.output || true) : false, 40 | stream: flags.stream, 41 | readDocs: flags.readDocs 42 | }); 43 | }); 44 | 45 | // Explain subcommand (registered as a separate command) 46 | cli 47 | .command('contract:explain ', 'Generate a technical explanation of a smart contract') 48 | .option('-m, --model [model]', 'Choose the AI model to use, omit value to select interactively') 49 | .option('--network ', 'Ethereum network (default: sepolia)', { default: 'sepolia' }) 50 | .option('-o, --output [dir]', 'Output directory for results, omit value to use default') 51 | .option('--no-stream', 'Disable streaming output') 52 | .option('--read-docs ', 'Read indexed docs collection as context') 53 | .action(async (source: string, flags: any) => { 54 | await explainContract(source, { 55 | model: flags.model, 56 | network: flags.network, 57 | output: flags.output !== false ? (flags.output || true) : false, 58 | stream: flags.stream, 59 | readDocs: flags.readDocs 60 | }); 61 | }); 62 | 63 | // Audit subcommand 64 | cli 65 | .command('contract:audit ', 'Perform a security audit of a smart contract') 66 | .option('-m, --model [model]', 'Choose the AI model to use, omit value to select interactively') 67 | .option('--network ', 'Ethereum network (default: sepolia)', { default: 'sepolia' }) 68 | .option('-o, --output [dir]', 'Output directory for results, omit value to use default') 69 | .option('--no-stream', 'Disable streaming output') 70 | .option('--read-docs ', 'Read indexed docs collection as context') 71 | .action(async (source: string, flags: any) => { 72 | await auditContract(source, { 73 | model: flags.model, 74 | network: flags.network, 75 | output: flags.output !== false ? (flags.output || true) : false, 76 | stream: flags.stream, 77 | readDocs: flags.readDocs 78 | }); 79 | }); 80 | 81 | // Custom query subcommand 82 | cli 83 | .command('contract:ask ', 'Ask a specific question about a smart contract') 84 | .option('-m, --model [model]', 'Choose the AI model to use, omit value to select interactively') 85 | .option('--network ', 'Ethereum network (default: sepolia)', { default: 'sepolia' }) 86 | .option('-o, --output [dir]', 'Output directory for results, omit value to use default') 87 | .option('--no-stream', 'Disable streaming output') 88 | .option('--read-docs ', 'Read indexed docs collection as context') 89 | .action(async (source: string, query: string, flags: any) => { 90 | await customContractRequest(source, query, { 91 | model: flags.model, 92 | network: flags.network, 93 | output: flags.output !== false ? (flags.output || true) : false, 94 | stream: flags.stream, 95 | readDocs: flags.readDocs 96 | }); 97 | }); 98 | } -------------------------------------------------------------------------------- /src/cli/commands/generate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart Contract Generation Command 3 | * 4 | * This module implements the 'generate' command for creating smart contracts 5 | * from natural language descriptions. 6 | */ 7 | import fs from 'node:fs'; 8 | import { Command as CliCommand } from 'cac'; 9 | import { readPipeInput } from '../../utils/tty.js'; 10 | import { generateContract } from '../../services/contract/generator.js'; 11 | import { runAgentMode } from '../../services/contract/agent-mode.js'; 12 | 13 | /** 14 | * Register the generate command with the CLI 15 | * 16 | * @param cli The CLI command instance 17 | * @returns The configured command 18 | */ 19 | export function registerGenerateCommand(cli: any): CliCommand { 20 | const command = cli 21 | .command('generate [...prompt]', 'Generate a smart contract from natural language') 22 | .option('-m, --model [model]', 'Choose the AI model to use, omit value to select interactively') 23 | .option('--files ', 'Add files to model context') 24 | .option('-u,--url ', 'Fetch URL content as context') 25 | .option('-s, --search', 'Enable web search focused on security best practices') 26 | .option('--no-stream', 'Disable streaming output') 27 | .option('--read-docs ', 'Read indexed docs collection as context') 28 | .option('-o, --output ', 'Output generated contract to a file') 29 | .option('--hardhat', 'Include Hardhat test file generation') 30 | .option('--agent', 'Use hierarchical multi-agent mode (experimental)') 31 | .option('--transparent-proxy', 'Generate upgradeable contract using OpenZeppelin Transparent Proxy pattern') 32 | .option('--uups-proxy', 'Generate upgradeable contract using UUPS proxy pattern') 33 | .action(async (prompt: string[], flags: any) => { 34 | const pipeInput = await readPipeInput(); 35 | const proxyType = flags.transparentProxy ? 'transparent' : flags.uupsProxy ? 'uups' : undefined; 36 | const extendedFlags = { ...flags, pipeInput, proxy: proxyType }; 37 | if (flags.agent) { 38 | console.log('➤ 🚀 Multi-agent mode enabled'); 39 | await runAgentMode(prompt.join(' '), extendedFlags); 40 | } else { 41 | await generateContract(prompt.join(' '), extendedFlags); 42 | } 43 | }); 44 | 45 | return command; 46 | } 47 | 48 | /** 49 | * Save generated contract to file 50 | * 51 | * @param code The contract code 52 | * @param filename The target filename 53 | */ 54 | export function saveContractToFile(code: string, filename: string): void { 55 | fs.writeFileSync(filename, code); 56 | console.log(`✅ Contract saved to ${filename}`); 57 | } -------------------------------------------------------------------------------- /src/cli/commands/vector-db.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Vector Database Commands 3 | * 4 | * This module implements commands for working with the vector database, 5 | * including adding documents, searching, and managing collections. 6 | */ 7 | 8 | import path from 'path'; 9 | import fs from 'fs'; 10 | import { VectorDB } from '../../services/vector-db/vector-db.js'; 11 | import { renderMarkdown } from '../../utils/markdown.js'; 12 | 13 | /** 14 | * Register vector database commands with the CLI 15 | * 16 | * @param cli The CLI command instance 17 | * @returns The configured command 18 | */ 19 | export function registerVectorDBCommands(cli: any): void { 20 | // Vector DB add-docs command 21 | cli 22 | .command('vdb-add-docs ', 'Add documents from a URL to the vector database') 23 | .option('-n, --name [name]', 'Collection name', { default: 'default' }) 24 | .option('--crawl', 'Recursively crawl the website') 25 | .option('--max-pages [number]', 'Maximum number of pages to crawl', { default: 30 }) 26 | .option('--max-depth [number]', 'Maximum crawl depth', { default: 3 }) 27 | .action(async (url: string, options: any) => { 28 | const db = new VectorDB(); 29 | 30 | console.log(`Adding documents from ${url} to collection '${options.name}'...`); 31 | 32 | try { 33 | const addedChunks = await db.addDocs(options.name, url, { 34 | crawl: options.crawl, 35 | maxPages: parseInt(options.maxPages), 36 | maxDepth: parseInt(options.maxDepth) 37 | }); 38 | 39 | console.log(`✅ Added ${addedChunks} document chunks to collection '${options.name}'`); 40 | } catch (error: any) { 41 | console.error(`Error adding documents: ${error.message || String(error)}`); 42 | } 43 | }); 44 | 45 | // Vector DB add-file command 46 | cli 47 | .command('vdb-add-file ', 'Add a file to the vector database') 48 | .option('-n, --name [name]', 'Collection name', { default: 'default' }) 49 | .option('-t, --title [title]', 'Document title') 50 | .action(async (filePath: string, options: any) => { 51 | const db = new VectorDB(); 52 | 53 | // Resolve absolute path 54 | const resolvedPath = path.resolve(process.cwd(), filePath); 55 | 56 | if (!fs.existsSync(resolvedPath)) { 57 | console.error(`File not found: ${resolvedPath}`); 58 | return; 59 | } 60 | 61 | console.log(`Adding file to collection '${options.name}'...`); 62 | 63 | try { 64 | const metadata = { 65 | title: options.title || path.basename(resolvedPath), 66 | type: 'file', 67 | extension: path.extname(resolvedPath) 68 | }; 69 | 70 | const addedChunks = await db.addFile(options.name, resolvedPath, metadata); 71 | 72 | console.log(`✅ Added ${addedChunks} document chunks to collection '${options.name}'`); 73 | } catch (error: any) { 74 | console.error(`Error adding file: ${error.message || String(error)}`); 75 | } 76 | }); 77 | 78 | // Vector DB search command 79 | cli 80 | .command('vdb-search ', 'Search the vector database') 81 | .option('-n, --name [name]', 'Collection name', { default: 'default' }) 82 | .option('-k [number]', 'Number of results to return', { default: 5 }) 83 | .option('--full-content', 'Show full content in results', { default: false }) 84 | .action(async (query: string, options: any) => { 85 | const db = new VectorDB(); 86 | 87 | console.log(`Searching in collection '${options.name}'...`); 88 | 89 | try { 90 | const results = await db.search(options.name, query, parseInt(options.k)); 91 | 92 | if (results.length === 0) { 93 | console.log(`No results found in collection '${options.name}'`); 94 | return; 95 | } 96 | 97 | console.log(`Found ${results.length} results:`); 98 | 99 | results.forEach((doc, i) => { 100 | const source = doc.metadata?.source || 'Unknown source'; 101 | const title = doc.metadata?.title || ''; 102 | 103 | // Format the source/title display 104 | console.log(`\n${i + 1}. ${renderMarkdown('**Source:**')} ${source}`); 105 | 106 | if (title && title !== source) { 107 | console.log(` ${renderMarkdown('**Title:**')} ${title}`); 108 | } 109 | 110 | // Additional metadata if available 111 | if (doc.metadata?.url && doc.metadata.url !== source) { 112 | console.log(` ${renderMarkdown('**URL:**')} ${doc.metadata.url}`); 113 | } 114 | 115 | if (doc.metadata?.crawlTime) { 116 | const crawlDate = new Date(doc.metadata.crawlTime); 117 | console.log(` ${renderMarkdown('**Indexed:**')} ${crawlDate.toLocaleString()}`); 118 | } 119 | 120 | // Display content with proper formatting 121 | console.log(renderMarkdown('---')); 122 | 123 | const hasValidContent = doc.pageContent && 124 | doc.pageContent.trim() !== '' && 125 | !doc.pageContent.includes('This document contains no extractable text content') && 126 | !doc.pageContent.includes('No content could be extracted from this page'); 127 | 128 | if (!hasValidContent) { 129 | console.log(renderMarkdown('*No content available*')); 130 | } else { 131 | // Normalize line breaks and whitespace 132 | let contentText = doc.pageContent 133 | .replace(/\r\n/g, '\n') 134 | .replace(/\n{3,}/g, '\n\n') 135 | .trim(); 136 | 137 | // Limit content length for better readability unless full content is requested 138 | const contentPreview = options.fullContent 139 | ? contentText 140 | : contentText.length > 800 141 | ? contentText.substring(0, 800) + "..." 142 | : contentText; 143 | 144 | console.log(renderMarkdown(contentPreview)); 145 | } 146 | 147 | console.log('-'.repeat(80)); 148 | }); 149 | } catch (error: any) { 150 | console.error(`Error searching: ${error.message || String(error)}`); 151 | } 152 | }); 153 | 154 | // Vector DB list collections command 155 | cli 156 | .command('vdb-list', 'List all collections in the vector database') 157 | .action(async () => { 158 | const db = new VectorDB(); 159 | 160 | try { 161 | const collections = await db.listCollections(); 162 | 163 | if (collections.length === 0) { 164 | console.log('No collections found'); 165 | return; 166 | } 167 | 168 | console.log(`Found ${collections.length} collections:`); 169 | 170 | collections.forEach((name, i) => { 171 | console.log(`${i + 1}. ${name}`); 172 | }); 173 | } catch (error: any) { 174 | console.error(`Error listing collections: ${error.message || String(error)}`); 175 | } 176 | }); 177 | 178 | // Vector DB fix collections command 179 | cli 180 | .command('vdb-fix', 'Fix and synchronize all collections in the vector database') 181 | .action(async () => { 182 | try { 183 | console.log('Initializing vector database repair...'); 184 | 185 | // Get the data directory path 186 | const dataDir = path.join(process.cwd(), ".vector-db"); 187 | if (!fs.existsSync(dataDir)) { 188 | console.log('No vector database directory found. Nothing to fix.'); 189 | return; 190 | } 191 | 192 | console.log(`Database directory found at: ${dataDir}`); 193 | 194 | // Scan for all collection files 195 | console.log('Scanning for collection files...'); 196 | const files = fs.readdirSync(dataDir); 197 | 198 | console.log(`Found ${files.length} files in the database directory: ${files.join(', ')}`); 199 | 200 | const collectionFiles = files 201 | .filter(file => file.endsWith('.json') && file !== 'collections.json') 202 | .map(file => file.replace('.json', '')); 203 | 204 | if (collectionFiles.length === 0) { 205 | console.log('No collection files found. Nothing to fix.'); 206 | return; 207 | } 208 | 209 | console.log(`Found ${collectionFiles.length} potential collections to repair: ${collectionFiles.join(', ')}`); 210 | 211 | // Create a fresh instance of the vector DB 212 | console.log('Creating vector database instance...'); 213 | const db = new VectorDB(); 214 | 215 | // Read current collections metadata 216 | const collectionsPath = path.join(dataDir, "collections.json"); 217 | let currentMetadata: string[] = []; 218 | 219 | if (fs.existsSync(collectionsPath)) { 220 | try { 221 | const metadataRaw = fs.readFileSync(collectionsPath, 'utf-8'); 222 | const metadata = JSON.parse(metadataRaw); 223 | currentMetadata = Object.keys(metadata); 224 | console.log(`Current collections in metadata: ${currentMetadata.join(', ')}`); 225 | } catch (error) { 226 | console.warn('Error reading collections metadata file:', error); 227 | } 228 | } else { 229 | console.log('No collections metadata file found.'); 230 | } 231 | 232 | // Find collections that need to be added to metadata 233 | const missingCollections = collectionFiles.filter(name => !currentMetadata.includes(name)); 234 | if (missingCollections.length > 0) { 235 | console.log(`Found ${missingCollections.length} collections missing from metadata: ${missingCollections.join(', ')}`); 236 | } else { 237 | console.log('All collection files are present in metadata.'); 238 | } 239 | 240 | // Load each collection to ensure it's registered 241 | let processed = 0; 242 | for (const name of collectionFiles) { 243 | console.log(`Processing collection: ${name}`); 244 | await db.getCollection(name); 245 | processed++; 246 | } 247 | 248 | // List collections after repair 249 | const collections = await db.listCollections(); 250 | console.log(`\n✅ Repair complete. Processed ${processed} collections.`); 251 | console.log(`Collections available: ${collections.join(', ')}`); 252 | 253 | } catch (error: any) { 254 | console.error(`Error fixing collections: ${error.message || String(error)}`); 255 | } 256 | }); 257 | 258 | // Vector DB add-text command 259 | cli 260 | .command('vdb-add-text ', 'Add text directly to the vector database') 261 | .option('-n, --name [name]', 'Collection name', { default: 'default' }) 262 | .option('-t, --title [title]', 'Document title') 263 | .option('-s, --source [source]', 'Source identifier') 264 | .action(async (text: string, options: any) => { 265 | const db = new VectorDB(); 266 | 267 | console.log(`Adding text to collection '${options.name}'...`); 268 | 269 | try { 270 | const metadata = { 271 | title: options.title || 'Text Document', 272 | source: options.source || 'User Input', 273 | type: 'text', 274 | timestamp: new Date().toISOString() 275 | }; 276 | 277 | const addedChunks = await db.addText(options.name, text, metadata); 278 | 279 | console.log(`✅ Added ${addedChunks} document chunks to collection '${options.name}'`); 280 | } catch (error: any) { 281 | console.error(`Error adding text: ${error.message || String(error)}`); 282 | } 283 | }); 284 | 285 | // Vector DB rebuild command (for debugging) 286 | cli 287 | .command('vdb-rebuild', 'Rebuild the vector database completely') 288 | .option('--clean', 'Delete all existing collections', { default: false }) 289 | .action(async (options: any) => { 290 | 291 | try { 292 | console.log('Rebuilding vector database...'); 293 | 294 | // Get the data directory path 295 | const dataDir = path.join(process.cwd(), ".vector-db"); 296 | 297 | // Option to remove all existing collections 298 | const cleanRebuild = options.clean === true; 299 | 300 | if (cleanRebuild) { 301 | console.log('Clean rebuild requested - removing all existing collections...'); 302 | 303 | if (fs.existsSync(dataDir)) { 304 | fs.rmSync(dataDir, { recursive: true, force: true }); 305 | } 306 | 307 | // Create a fresh directory 308 | fs.mkdirSync(dataDir, { recursive: true }); 309 | } else { 310 | console.log('Preserving existing collections...'); 311 | 312 | // Ensure the directory exists 313 | if (!fs.existsSync(dataDir)) { 314 | fs.mkdirSync(dataDir, { recursive: true }); 315 | } 316 | } 317 | 318 | // Add a test document 319 | const testCollection = 'rebuild-test'; 320 | 321 | const testText = ` 322 | # Smart Contracts 323 | 324 | Smart contracts are programs which govern the behaviour of accounts within the Ethereum state. 325 | 326 | ## Definition 327 | 328 | Smart contracts are collections of code (its functions) and data (its state) that reside at a specific 329 | address on the Ethereum blockchain. Smart contracts are a type of Ethereum account. This means they 330 | have a balance and can be the target of transactions. However, they're not controlled by a user, 331 | instead they are deployed to the network and run as programmed. User accounts can then interact 332 | with a smart contract by submitting transactions that execute a function defined on the smart contract. 333 | Smart contracts can define rules, like a regular contract, and automatically enforce them via the code. 334 | 335 | ## Key Features 336 | 337 | 1. **Immutable**: Once deployed, the code of a smart contract cannot be changed. 338 | 2. **Deterministic**: The same input will always produce the same output. 339 | 3. **Trustless**: No need to trust a third party, as the contract enforces its own rules. 340 | 4. **Transparent**: All transactions on the blockchain are publicly visible. 341 | `; 342 | 343 | // Read existing collections.json if it exists and we're preserving collections 344 | let collectionsData: Record = {}; 345 | const collectionsPath = path.join(dataDir, "collections.json"); 346 | 347 | if (!cleanRebuild && fs.existsSync(collectionsPath)) { 348 | try { 349 | const existingData = fs.readFileSync(collectionsPath, "utf-8"); 350 | collectionsData = JSON.parse(existingData); 351 | console.log(`Found existing collections: ${Object.keys(collectionsData).join(', ')}`); 352 | } catch (error) { 353 | console.warn('Could not read existing collections data, starting fresh.'); 354 | } 355 | } 356 | 357 | // Add the test collection to the metadata 358 | collectionsData[testCollection] = { 359 | name: testCollection, 360 | timestamp: new Date().toISOString() 361 | }; 362 | 363 | // Save collections metadata 364 | fs.writeFileSync( 365 | collectionsPath, 366 | JSON.stringify(collectionsData, null, 2) 367 | ); 368 | 369 | // Create document file for test collection 370 | const documents = [ 371 | { 372 | pageContent: testText, 373 | metadata: { 374 | source: 'rebuild-test', 375 | title: 'Smart Contract Definition' 376 | } 377 | } 378 | ]; 379 | 380 | // Save document directly 381 | fs.writeFileSync( 382 | path.join(dataDir, `${testCollection}.json`), 383 | JSON.stringify(documents, null, 2) 384 | ); 385 | 386 | console.log(`✅ Added document to test collection`); 387 | console.log('Vector database has been successfully rebuilt'); 388 | 389 | // Let the vector DB service discover and load all collections 390 | const vectorDB = new VectorDB(); 391 | const collections = await vectorDB.listCollections(); 392 | 393 | console.log(`Collections available: ${collections.join(', ')}`); 394 | } catch (error: any) { 395 | console.error(`Error rebuilding database: ${error.message || String(error)}`); 396 | } 397 | }); 398 | } -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Web3CLI - Command Line Interface 4 | * 5 | * Main entry point for the CLI application. 6 | */ 7 | import process from "node:process"; 8 | import { cac } from "cac"; 9 | import { loadConfig } from "../services/config/config.js"; 10 | import { registerGenerateCommand } from "./commands/generate.js"; 11 | import updateNotifier from "update-notifier"; 12 | 13 | import { fail } from "../utils/logger.js"; 14 | 15 | // Package version info (injected by build process) 16 | declare const PKG_NAME: string; 17 | declare const PKG_VERSION: string; 18 | 19 | /** 20 | * Main entry point for the CLI 21 | */ 22 | async function main() { 23 | // Check for updates 24 | if (typeof PKG_NAME === "string" && typeof PKG_VERSION === "string") { 25 | updateNotifier({ 26 | pkg: { name: PKG_NAME, version: PKG_VERSION }, 27 | shouldNotifyInNpmScript: false, 28 | }).notify({ 29 | isGlobal: true, 30 | }); 31 | } 32 | 33 | // Initialize CLI 34 | const cli = cac("web3cli"); 35 | loadConfig(); 36 | 37 | // Register commands 38 | registerGenerateCommand(cli); 39 | // Add help and version 40 | cli.help(); 41 | cli.version(PKG_VERSION || "0.0.0"); 42 | 43 | // Parse and execute command 44 | try { 45 | cli.parse(process.argv, { run: false }); 46 | await cli.runMatchedCommand(); 47 | } catch (error) { 48 | // Unified clean error logging 49 | const msg = error instanceof Error ? error.message : String(error); 50 | fail(`Error: ${msg}`); 51 | process.exit(1); 52 | } 53 | } 54 | 55 | // Run the main function 56 | main().catch((error) => { 57 | const msg = error instanceof Error ? error.message : String(error); 58 | fail(`Error: ${msg}`); 59 | process.exit(1); 60 | }); -------------------------------------------------------------------------------- /src/services/ai/ai-command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AI Command Management 3 | * 4 | * This module provides functionality for managing AI-powered commands. 5 | */ 6 | import { loadConfig, AICommand } from "../config/config.js" 7 | import { runCommand } from "../../utils/common.js" 8 | import cliPrompts from "prompts" 9 | import { stdin } from "../../utils/tty.js" 10 | 11 | /** 12 | * Get all commands defined in config 13 | * 14 | * @returns Array of AICommand objects 15 | */ 16 | export async function getAllCommands(): Promise { 17 | const config = loadConfig() 18 | return config.commands || [] 19 | } 20 | 21 | /** 22 | * Process a command's variables 23 | * 24 | * @param command The AICommand object 25 | * @returns Variables map with values 26 | */ 27 | export async function getVariables(command: AICommand): Promise> { 28 | const variables: Record = {} 29 | 30 | // If no variables defined, return empty object 31 | if (!command.variables) return variables 32 | 33 | for (const [key, value] of Object.entries(command.variables)) { 34 | if (typeof value === "string") { 35 | variables[key] = await runCommand(value) 36 | continue 37 | } 38 | 39 | if (value.type === "input") { 40 | const { value: input } = await cliPrompts({ 41 | type: "text", 42 | name: "value", 43 | message: value.message, 44 | stdin, 45 | }) 46 | variables[key] = input 47 | continue 48 | } 49 | 50 | if (value.type === "select") { 51 | const { value: selected } = await cliPrompts({ 52 | type: "select", 53 | name: "value", 54 | message: value.message, 55 | choices: value.choices, 56 | stdin, 57 | }) 58 | variables[key] = selected 59 | continue 60 | } 61 | } 62 | 63 | return variables 64 | } 65 | 66 | /** 67 | * Get the prompt for a command 68 | * 69 | * @param command The AICommand object 70 | * @param variables Variables map with values 71 | * @returns Formatted prompt 72 | */ 73 | export function getPrompt(command: AICommand, variables: Record): string { 74 | let prompt = command.prompt 75 | 76 | for (const [key, value] of Object.entries(variables)) { 77 | prompt = prompt.replace(new RegExp(`{${key}}`, "g"), value) 78 | } 79 | 80 | return prompt 81 | } -------------------------------------------------------------------------------- /src/services/ai/ai-sdk.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AI SDK Utilities 3 | * 4 | * This module provides utilities for working with different AI model providers. 5 | */ 6 | import OpenAI from "openai" 7 | import { getModelProvider, getRealModelId } from "./models.js" 8 | import { Config, configDirPath } from "../config/config.js" 9 | import path from "node:path" 10 | 11 | import { GoogleGenerativeAI } from "@google/generative-ai" 12 | import { Anthropic } from "@anthropic-ai/sdk" 13 | 14 | /** 15 | * Get the SDK model based on the model ID 16 | * 17 | * @param modelId The model ID 18 | * @param config Configuration object 19 | * @returns Model client 20 | */ 21 | export async function getSDKModel(modelId: string, config: Config) { 22 | const provider = getModelProvider(modelId); 23 | const realModelId = getRealModelId(modelId); 24 | 25 | try { 26 | switch (provider) { 27 | case "anthropic": 28 | return getAnthropicClient(config); 29 | case "gemini": 30 | return getGeminiClient(config, realModelId); 31 | case "groq": 32 | return getGroqClient(config); 33 | case "mistral": 34 | return getMistralClient(config); 35 | case "copilot": 36 | return getCopilotClient(config); 37 | case "ollama": 38 | return getOllamaClient(config); 39 | case "openai": 40 | default: 41 | return getOpenAIClient(config); 42 | } 43 | } catch (error) { 44 | const e = error as Error; 45 | if (e.message.includes("API key not found")) { 46 | const localPath = path.join(process.cwd(), "web3cli.toml"); 47 | const globalPath = path.join(configDirPath, "web3cli.toml"); 48 | throw new Error( 49 | `${provider.charAt(0).toUpperCase() + provider.slice(1)} API key not configured. ` + 50 | `Please set the ${provider.toUpperCase()}_API_KEY environment variable, or add ${provider.toLowerCase()}_api_key ` + 51 | `to your web3cli.toml configuration file (${localPath} or ${globalPath}).` 52 | ); 53 | } 54 | throw error; 55 | } 56 | } 57 | 58 | /** 59 | * Get OpenAI client 60 | * 61 | * @param config Configuration object 62 | * @returns OpenAI client 63 | */ 64 | function getOpenAIClient(config: Config) { 65 | if (!config.openai_api_key) { 66 | const localPath = path.join(process.cwd(), "web3cli.toml"); 67 | const globalPath = path.join(configDirPath, "web3cli.toml"); 68 | throw new Error( 69 | `OpenAI API key not found. Please set the OPENAI_API_KEY environment variable, ` + 70 | `or add openai_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` 71 | ); 72 | } 73 | 74 | const baseURL = config.openai_api_url || process.env.OPENAI_API_URL 75 | const openaiOptions: any = { 76 | apiKey: config.openai_api_key, 77 | } 78 | 79 | if (baseURL) { 80 | openaiOptions.baseURL = baseURL 81 | } 82 | 83 | return new OpenAI(openaiOptions) 84 | } 85 | 86 | /** 87 | * Get Anthropic client 88 | * 89 | * @param config Configuration object 90 | * @returns Anthropic client 91 | */ 92 | function getAnthropicClient(config: Config) { 93 | if (!config.anthropic_api_key) { 94 | const localPath = path.join(process.cwd(), "web3cli.toml"); 95 | const globalPath = path.join(configDirPath, "web3cli.toml"); 96 | throw new Error( 97 | `Anthropic API key not found. Please set the ANTHROPIC_API_KEY environment variable, ` + 98 | `or add anthropic_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` 99 | ); 100 | } 101 | 102 | const anthropic = new Anthropic({ 103 | apiKey: config.anthropic_api_key, 104 | }); 105 | 106 | // Return an adapter with OpenAI-like interface for Anthropic 107 | return { 108 | chat: { 109 | completions: { 110 | create: async ({ messages, stream = false, ...options }: any) => { 111 | try { 112 | // Convert OpenAI format to Anthropic format 113 | let systemPrompt = ""; 114 | const anthropicMessages = messages.map((msg: any) => { 115 | if (msg.role === "system") { 116 | systemPrompt = msg.content; 117 | return null; // Will be filtered out below 118 | } 119 | return { 120 | role: msg.role === "assistant" ? "assistant" : "user", 121 | content: msg.content 122 | }; 123 | }).filter(Boolean); 124 | 125 | if (stream) { 126 | const streamingResponse = await anthropic.beta.messages.create({ 127 | model: options.model || "claude-3-5-sonnet-20240620", 128 | messages: anthropicMessages, 129 | system: systemPrompt, 130 | stream: true, 131 | max_tokens: options.max_tokens || 4096, 132 | temperature: options.temperature || 0, 133 | }); 134 | 135 | // Create an AsyncIterable that mimics OpenAI's stream format 136 | return { 137 | [Symbol.asyncIterator]: async function*() { 138 | for await (const chunk of streamingResponse) { 139 | if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') { 140 | yield { 141 | choices: [{ 142 | delta: { content: chunk.delta.text } 143 | }] 144 | }; 145 | } 146 | } 147 | } 148 | }; 149 | } else { 150 | const response = await anthropic.beta.messages.create({ 151 | model: options.model || "claude-3-5-sonnet-20240620", 152 | messages: anthropicMessages, 153 | system: systemPrompt, 154 | max_tokens: options.max_tokens || 4096, 155 | temperature: options.temperature || 0, 156 | }); 157 | 158 | return { 159 | choices: [{ 160 | message: { 161 | content: response.content[0].type === 'text' ? response.content[0].text : response.content[0] 162 | } 163 | }] 164 | }; 165 | } 166 | } catch (error) { 167 | console.error("Anthropic API error:", error); 168 | throw error; 169 | } 170 | } 171 | } 172 | } 173 | }; 174 | } 175 | 176 | /** 177 | * Get Gemini client 178 | * 179 | * @param config Configuration object 180 | * @param modelName The actual model name to use 181 | * @returns Gemini client with chat interface similar to OpenAI 182 | */ 183 | function getGeminiClient(config: Config, modelName: string) { 184 | if (!config.gemini_api_key) { 185 | const localPath = path.join(process.cwd(), "web3cli.toml"); 186 | const globalPath = path.join(configDirPath, "web3cli.toml"); 187 | throw new Error( 188 | `Gemini API key not found. Please set the GEMINI_API_KEY environment variable, ` + 189 | `or add gemini_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` 190 | ); 191 | } 192 | 193 | // Initialize the Gemini API 194 | const genAI = new GoogleGenerativeAI(config.gemini_api_key); 195 | const model = genAI.getGenerativeModel({ model: modelName }); 196 | 197 | // Return an adapter with OpenAI-like interface 198 | return { 199 | chat: { 200 | completions: { 201 | create: async ({ messages, stream = false }: any) => { 202 | // Convert OpenAI-style messages to Gemini format 203 | const prompt = messages.map((msg: any) => { 204 | if (msg.role === "system") { 205 | return { role: "user", parts: [{ text: msg.content }] }; 206 | } 207 | return { 208 | role: msg.role === "assistant" ? "model" : "user", 209 | parts: [{ text: msg.content }] 210 | }; 211 | }); 212 | 213 | try { 214 | if (stream) { 215 | const streamingResponse = await model.generateContentStream({ contents: prompt }); 216 | 217 | // Create an AsyncIterable that mimics OpenAI's stream format 218 | return { 219 | [Symbol.asyncIterator]: async function*() { 220 | for await (const chunk of streamingResponse.stream) { 221 | const text = chunk.text(); 222 | yield { 223 | choices: [{ 224 | delta: { content: text } 225 | }] 226 | }; 227 | } 228 | } 229 | }; 230 | } else { 231 | const response = await model.generateContent({ contents: prompt }); 232 | return { 233 | choices: [{ 234 | message: { 235 | content: response.response.text() 236 | } 237 | }] 238 | }; 239 | } 240 | } catch (error) { 241 | console.error("Gemini API error:", error); 242 | throw error; 243 | } 244 | } 245 | } 246 | } 247 | }; 248 | } 249 | 250 | /** 251 | * Get Groq client 252 | * 253 | * @param config Configuration object 254 | * @returns Groq client (using OpenAI-compatible API) 255 | */ 256 | function getGroqClient(config: Config) { 257 | if (!config.groq_api_key) { 258 | const localPath = path.join(process.cwd(), "web3cli.toml"); 259 | const globalPath = path.join(configDirPath, "web3cli.toml"); 260 | throw new Error( 261 | `Groq API key not found. Please set the GROQ_API_KEY environment variable, ` + 262 | `or add groq_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` 263 | ); 264 | } 265 | 266 | // Groq uses the OpenAI-compatible API 267 | const baseURL = config.groq_api_url || "https://api.groq.com/openai/v1"; 268 | return new OpenAI({ 269 | apiKey: config.groq_api_key, 270 | baseURL: baseURL 271 | }); 272 | } 273 | 274 | /** 275 | * Get Mistral client 276 | * 277 | * @param config Configuration object 278 | * @returns Mistral client (using OpenAI-compatible API) 279 | */ 280 | function getMistralClient(config: Config) { 281 | if (!config.mistral_api_key) { 282 | const localPath = path.join(process.cwd(), "web3cli.toml"); 283 | const globalPath = path.join(configDirPath, "web3cli.toml"); 284 | throw new Error( 285 | `Mistral API key not found. Please set the MISTRAL_API_KEY environment variable, ` + 286 | `or add mistral_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` 287 | ); 288 | } 289 | 290 | // Mistral uses the OpenAI-compatible API 291 | const baseURL = config.mistral_api_url || "https://api.mistral.ai/v1"; 292 | return new OpenAI({ 293 | apiKey: config.mistral_api_key, 294 | baseURL: baseURL 295 | }); 296 | } 297 | 298 | /** 299 | * Get GitHub Copilot client 300 | * 301 | * @param config Configuration object 302 | * @returns Copilot client (or OpenAI as fallback for now) 303 | */ 304 | function getCopilotClient(config: Config) { 305 | // For now, use OpenAI client as a stub 306 | // In a real implementation, this would return a Copilot client 307 | console.warn("Using OpenAI as a fallback for Copilot models - proper Copilot API access not implemented") 308 | return getOpenAIClient(config); 309 | } 310 | 311 | /** 312 | * Get Ollama client 313 | * 314 | * @param config Configuration object 315 | * @returns Ollama client with OpenAI-compatible interface 316 | */ 317 | function getOllamaClient(config: Config) { 318 | const host = config.ollama_host || "http://localhost:11434"; 319 | 320 | // Create an adapter with OpenAI-like interface for Ollama 321 | return { 322 | chat: { 323 | completions: { 324 | create: async ({ messages, stream = false, model: modelName, ...options }: any) => { 325 | try { 326 | // Convert OpenAI messages format to Ollama format 327 | const ollama_messages = messages.map((msg: any) => { 328 | // Ollama doesn't support system messages directly, 329 | // so convert to a user message if needed 330 | return { 331 | role: msg.role === "system" ? "user" : msg.role, 332 | content: msg.content 333 | }; 334 | }); 335 | 336 | // Extract model from the real model ID if provided 337 | const modelToUse = modelName || "llama3"; 338 | 339 | if (stream) { 340 | // Initialize fetch for streaming response 341 | const response = await fetch(`${host}/api/chat`, { 342 | method: 'POST', 343 | headers: { 344 | 'Content-Type': 'application/json', 345 | }, 346 | body: JSON.stringify({ 347 | model: modelToUse, 348 | messages: ollama_messages, 349 | stream: true, 350 | options: { 351 | temperature: options.temperature || 0, 352 | }, 353 | }), 354 | }); 355 | 356 | if (!response.ok) { 357 | throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); 358 | } 359 | 360 | if (!response.body) { 361 | throw new Error('Ollama response body is null'); 362 | } 363 | 364 | const reader = response.body.getReader(); 365 | const decoder = new TextDecoder(); 366 | 367 | // Create an AsyncIterable that mimics OpenAI's stream format 368 | return { 369 | [Symbol.asyncIterator]: async function*() { 370 | while (true) { 371 | const { done, value } = await reader.read(); 372 | if (done) break; 373 | 374 | const chunk = decoder.decode(value); 375 | // Ollama sends JSON objects, each on a new line 376 | const lines = chunk.split('\n').filter(Boolean); 377 | 378 | for (const line of lines) { 379 | try { 380 | const data = JSON.parse(line); 381 | if (data.message?.content) { 382 | yield { 383 | choices: [{ 384 | delta: { content: data.message.content } 385 | }] 386 | }; 387 | } 388 | } catch (e) { 389 | console.warn('Failed to parse Ollama chunk:', line); 390 | } 391 | } 392 | } 393 | } 394 | }; 395 | } else { 396 | // Non-streaming request 397 | const response = await fetch(`${host}/api/chat`, { 398 | method: 'POST', 399 | headers: { 400 | 'Content-Type': 'application/json', 401 | }, 402 | body: JSON.stringify({ 403 | model: modelToUse, 404 | messages: ollama_messages, 405 | options: { 406 | temperature: options.temperature || 0, 407 | }, 408 | }), 409 | }); 410 | 411 | if (!response.ok) { 412 | throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); 413 | } 414 | 415 | const data = await response.json(); 416 | 417 | return { 418 | choices: [{ 419 | message: { 420 | content: data.message?.content || "No content returned from Ollama" 421 | } 422 | }] 423 | }; 424 | } 425 | } catch (error) { 426 | console.error("Ollama API error:", error); 427 | throw error; 428 | } 429 | } 430 | } 431 | } 432 | }; 433 | } -------------------------------------------------------------------------------- /src/services/ai/ask.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ask Service 3 | * 4 | * This module provides functionality for asking questions to AI models. 5 | */ 6 | import process from "node:process" 7 | import { ChatCompletionMessageParam } from "openai/resources/chat/completions" 8 | import { loadFiles, notEmpty } from "../../utils/common.js" 9 | import { loadConfig } from "../config/config.js" 10 | import { 11 | getAllModels, 12 | getModelProvider 13 | } from "./models.js" 14 | import cliPrompts from "prompts" 15 | import { stdin } from "../../utils/tty.js" 16 | import { CliError } from "../../utils/error.js" 17 | import { getSDKModel } from "./ai-sdk.js" 18 | import { fetchUrl } from "../../utils/fetch-url.js" 19 | import logUpdate from "log-update" 20 | import { renderMarkdown } from "../../utils/markdown.js" 21 | import { processVectorDBReadRequest } from "./rag-utils.js" 22 | 23 | // Function to replace debug module 24 | const debug = (...args: any[]) => { 25 | if (process.env.DEBUG !== "shell-ask" && process.env.DEBUG !== "*") return 26 | console.log(...args) 27 | } 28 | 29 | /** 30 | * Ask a question to an AI model 31 | * 32 | * @param prompt The question prompt 33 | * @param options Options for the question 34 | */ 35 | export async function ask( 36 | prompt: string | undefined, 37 | options: { 38 | model?: string | boolean 39 | command?: boolean 40 | pipeInput?: string 41 | files?: string | string[] 42 | type?: string 43 | url?: string | string[] 44 | search?: boolean 45 | stream?: boolean 46 | breakdown?: boolean 47 | readDocs?: string 48 | [key: string]: any 49 | } 50 | ) { 51 | if (!prompt) { 52 | throw new CliError("please provide a prompt") 53 | } 54 | 55 | const config = loadConfig() 56 | let modelId = 57 | options.model === true 58 | ? "select" 59 | : options.model || 60 | config.default_model || 61 | "gpt-4o-mini" 62 | 63 | // Include Ollama models when explicitly requested or when selecting from all models 64 | const includeOllama = modelId === "select" || modelId.startsWith("ollama-"); 65 | 66 | const models = await getAllModels( 67 | modelId === "select" ? true : false, 68 | includeOllama 69 | ) 70 | 71 | if (modelId === "select") { 72 | if (process.platform === "win32" && !process.stdin.isTTY) { 73 | throw new CliError( 74 | "Interactively selecting a model is not supported on Windows when using piped input. Consider directly specifying the model id instead, for example: `-m gpt-4o`" 75 | ) 76 | } 77 | 78 | const result = await cliPrompts([ 79 | { 80 | stdin, 81 | 82 | type: "autocomplete", 83 | 84 | message: "Select a model", 85 | 86 | name: "modelId", 87 | 88 | async suggest(input, choices) { 89 | return choices.filter((choice) => { 90 | return choice.title.toLowerCase().includes(input) 91 | }) 92 | }, 93 | 94 | choices: models 95 | .filter( 96 | (item) => modelId === "select" || item.id.startsWith(`${modelId}-`) 97 | ) 98 | .map((item) => { 99 | return { 100 | value: item.id, 101 | title: item.id, 102 | } 103 | }), 104 | }, 105 | ]) 106 | 107 | if (typeof result.modelId !== "string" || !result.modelId) { 108 | throw new CliError("no model selected") 109 | } 110 | 111 | modelId = result.modelId 112 | } 113 | 114 | debug(`Selected modelID: ${modelId}`) 115 | 116 | const matchedModel = models.find( 117 | (m) => m.id === modelId || m.realId === modelId 118 | ) 119 | if (!matchedModel) { 120 | // Get a list of models by prefix to suggest alternatives 121 | const modelPrefix = modelId.split('-')[0]; 122 | const similarModels = models 123 | .filter(m => m.id.startsWith(`${modelPrefix}-`)) 124 | .map(m => m.id); 125 | 126 | let errorMessage = `Model not found: ${modelId}\n\n`; 127 | 128 | if (similarModels.length > 0) { 129 | errorMessage += `Did you mean one of these models?\n${similarModels.join('\n')}\n\n`; 130 | } 131 | 132 | errorMessage += `Available models: ${models.map((m) => m.id).join(', ')}`; 133 | 134 | throw new CliError(errorMessage); 135 | } 136 | const realModelId = matchedModel.realId || modelId 137 | const openai = await getSDKModel(modelId, config) 138 | 139 | debug("model", realModelId) 140 | 141 | const files = await loadFiles(options.files || []) 142 | const remoteContents = await fetchUrl(options.url || []) 143 | 144 | // Handle vector DB docs 145 | let docsContext: string[] = [] 146 | if (options.readDocs) { 147 | try { 148 | // Get relevant content from the vector DB using our new utility 149 | const docsContent = await processVectorDBReadRequest(prompt, options.readDocs, 8) 150 | if (docsContent) { 151 | docsContext = [ 152 | `docs:${options.readDocs}:`, 153 | `""" 154 | ${docsContent} 155 | """` 156 | ] 157 | } 158 | } catch (e) { 159 | // ignore if vector db fails 160 | console.warn("Warning: Failed to retrieve docs from vector DB", e) 161 | } 162 | } 163 | 164 | const context = [ 165 | `platform: ${process.platform}\nshell: ${process.env.SHELL || "unknown"}`, 166 | 167 | options.pipeInput && [`stdin:`, "```", options.pipeInput, "```"].join("\n"), 168 | 169 | files.length > 0 && "files:", 170 | ...files.map((file) => `${file.name}:\n"""\n${file.content}\n"""`), 171 | 172 | remoteContents.length > 0 && "remote contents:", 173 | ...remoteContents.map( 174 | (content) => `${content.url}: 175 | """ 176 | ${content.content} 177 | """` 178 | ), 179 | ...docsContext, 180 | ] 181 | .filter(notEmpty) 182 | .join("\n") 183 | 184 | let searchResult: string | undefined 185 | 186 | if (options.search) { 187 | // Skip search for now as it depends on SDK compatibility 188 | console.log("Web search is not currently available") 189 | } 190 | 191 | const systemMessage = `You are a Web3 development expert specializing in blockchain technologies, smart contracts, and decentralized applications. You provide accurate, helpful information about Solidity, Ethereum, and related technologies.` 192 | 193 | const userMessage = [ 194 | searchResult && `SEARCH RESULTS:\n${searchResult}`, 195 | context && `CONTEXT:\n${context}`, 196 | `QUESTION: ${prompt}`, 197 | ] 198 | .filter(Boolean) 199 | .join("\n\n") 200 | 201 | try { 202 | let content = "" 203 | 204 | // Prepare base messages for OpenAI 205 | let messages: ChatCompletionMessageParam[] = [ 206 | { 207 | role: "system", 208 | content: systemMessage, 209 | }, 210 | { 211 | role: "user", 212 | content: userMessage, 213 | } 214 | ] 215 | 216 | // Augment with RAG if readDocs is provided 217 | if (options.readDocs) { 218 | console.log(`Using RAG with collection: ${options.readDocs}`); 219 | 220 | try { 221 | // Get relevant content directly rather than using the augmentation utility 222 | const docsContent = await processVectorDBReadRequest(prompt, options.readDocs, 5); 223 | 224 | if (docsContent) { 225 | // Add the content to the user message 226 | messages[1] = { 227 | role: "user", 228 | content: `${userMessage}\n\nHere's some relevant information to help you answer:\n\n${docsContent}` 229 | }; 230 | console.log("✅ Context from vector database added to prompt"); 231 | } 232 | } catch (error) { 233 | console.warn("Warning: Failed to augment messages with RAG", error); 234 | } 235 | } 236 | 237 | const provider = getModelProvider(modelId); 238 | console.log(`Using ${provider.toUpperCase()} provider with model: ${realModelId}`); 239 | 240 | if (options.stream !== false) { 241 | try { 242 | const stream = await openai.chat.completions.create({ 243 | model: realModelId, 244 | messages, 245 | stream: true, 246 | }); 247 | 248 | // Type assertion to ensure the stream has the async iterator 249 | const streamWithIterator = stream as AsyncIterable; 250 | 251 | for await (const chunk of streamWithIterator) { 252 | const content_chunk = chunk.choices?.[0]?.delta?.content || ""; 253 | content += content_chunk; 254 | logUpdate(renderMarkdown(content)); 255 | } 256 | 257 | logUpdate.done(); 258 | } catch (error: any) { 259 | logUpdate.clear(); 260 | 261 | if (error.message && error.message.includes("does not exist")) { 262 | console.error(`Error: Model '${realModelId}' not found or not available with the ${provider} provider.`); 263 | console.error(`\nMake sure you've configured the API key for ${provider} and are using a valid model.`); 264 | console.error(`To see all available models, run: web3cli list`); 265 | } else { 266 | console.error("Error during streaming request:", error.message || error); 267 | } 268 | } 269 | } else { 270 | try { 271 | const completion = await openai.chat.completions.create({ 272 | model: realModelId, 273 | messages, 274 | }); 275 | 276 | // Handle potentially undefined choices 277 | content = completion.choices?.[0]?.message?.content || "No response generated"; 278 | console.log(renderMarkdown(content)); 279 | } catch (error: any) { 280 | if (error.message && error.message.includes("does not exist")) { 281 | console.error(`Error: Model '${realModelId}' not found or not available with the ${provider} provider.`); 282 | console.error(`\nMake sure you've configured the API key for ${provider} and are using a valid model.`); 283 | console.error(`To see all available models, run: web3cli list`); 284 | } else { 285 | console.error("Error during request:", error.message || error); 286 | } 287 | } 288 | } 289 | } catch (error) { 290 | console.error("Error during request:", error); 291 | } 292 | } -------------------------------------------------------------------------------- /src/services/ai/client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AI Client Service 3 | * 4 | * This module provides a unified interface for interacting with AI models 5 | * from different providers (OpenAI, Anthropic, etc.) 6 | */ 7 | import { openai, anthropic, gemini, groq, mistral, copilot, ollama } from './mastra-shim.js'; 8 | import { loadConfig } from '../config/config.js'; 9 | import { getModelProvider, getRealModelId } from './models.js'; 10 | 11 | /** 12 | * Result from an AI generation 13 | */ 14 | export interface AIResult { 15 | text: string; 16 | finishReason?: string; 17 | usage?: { 18 | promptTokens: number; 19 | completionTokens: number; 20 | totalTokens: number; 21 | }; 22 | } 23 | 24 | /** 25 | * Options for AI generation 26 | */ 27 | export interface AIOptions { 28 | stream?: boolean; 29 | temperature?: number; 30 | maxTokens?: number; 31 | } 32 | 33 | /** 34 | * AI Client interface 35 | */ 36 | export interface AIClient { 37 | generate(prompt: string, options?: AIOptions): Promise; 38 | generateStream?(prompt: string, options?: AIOptions): AsyncIterable; 39 | } 40 | 41 | /** 42 | * Default AI Client implementation 43 | */ 44 | class DefaultAIClient implements AIClient { 45 | private model: string; 46 | private provider: string; 47 | 48 | constructor(model: string, provider: string) { 49 | this.model = model; 50 | this.provider = provider; 51 | } 52 | 53 | /** 54 | * Generate text from a prompt 55 | * 56 | * @param prompt The prompt to generate from 57 | * @param options Generation options 58 | * @returns Generated text and metadata 59 | */ 60 | async generate(prompt: string): Promise { 61 | console.log(`Generating with ${this.provider} model: ${this.model}`); 62 | 63 | // This is a stub implementation 64 | // In a real implementation, this would call the actual AI API 65 | 66 | return { 67 | text: `This is a stub response for: "${prompt}"`, 68 | finishReason: "stop", 69 | usage: { 70 | promptTokens: 100, 71 | completionTokens: 200, 72 | totalTokens: 300, 73 | }, 74 | }; 75 | } 76 | } 77 | 78 | /** 79 | * Get an AI client for the specified model 80 | * 81 | * @param modelOverride Override the model from config 82 | * @returns AI client 83 | */ 84 | export function getAIClient(modelOverride?: string): AIClient { 85 | const config = loadConfig(); 86 | const modelName = modelOverride || config.default_model || "gpt-4o-mini"; 87 | 88 | // Determine provider based on model name 89 | const provider = getModelProvider(modelName); 90 | const realModelId = getRealModelId(modelName); 91 | 92 | // Get the provider-specific model identifier 93 | let clientModel: string; 94 | 95 | switch (provider) { 96 | case "anthropic": 97 | clientModel = anthropic(realModelId); 98 | break; 99 | case "gemini": 100 | clientModel = gemini(realModelId); 101 | break; 102 | case "groq": 103 | clientModel = groq(realModelId); 104 | break; 105 | case "mistral": 106 | clientModel = mistral(realModelId); 107 | break; 108 | case "copilot": 109 | clientModel = copilot(realModelId); 110 | break; 111 | case "ollama": 112 | clientModel = ollama(realModelId); 113 | break; 114 | case "openai": 115 | default: 116 | clientModel = openai(realModelId); 117 | break; 118 | } 119 | 120 | return new DefaultAIClient(clientModel, provider); 121 | } -------------------------------------------------------------------------------- /src/services/ai/mastra-shim.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mastra AI Framework Integration 3 | * 4 | * This module provides integration with the Mastra AI agent framework, 5 | * exposing interfaces for creating and managing AI agents. 6 | */ 7 | import { getSDKModel } from "./ai-sdk.js"; 8 | import { loadConfig } from "../config/config.js"; 9 | import { ChatCompletionMessageParam } from "openai/resources/chat/completions"; 10 | 11 | /** 12 | * Agent configuration options 13 | */ 14 | export interface AgentConfig { 15 | name: string; 16 | instructions: string; 17 | model: string; 18 | tools?: Record; 19 | } 20 | 21 | /** 22 | * Agent class for AI-based agents 23 | */ 24 | export class Agent { 25 | name: string; 26 | instructions: string; 27 | model: string; 28 | tools: Record; 29 | 30 | constructor(config: AgentConfig) { 31 | this.name = config.name; 32 | this.instructions = config.instructions; 33 | this.model = config.model; 34 | this.tools = config.tools || {}; 35 | } 36 | 37 | /** 38 | * Run the agent with the given input 39 | * 40 | * @param input Input for the agent 41 | * @returns Agent output 42 | */ 43 | async run(input: any): Promise { 44 | console.log(`[${this.name}] Running with model: ${this.model}`); 45 | 46 | try { 47 | const config = loadConfig(); 48 | const openai = await getSDKModel(this.model, config); 49 | 50 | // Create system and user messages 51 | const messages: ChatCompletionMessageParam[] = [ 52 | { 53 | role: "system", 54 | content: this.instructions 55 | }, 56 | { 57 | role: "user", 58 | content: typeof input === 'string' ? input : JSON.stringify(input, null, 2) 59 | } 60 | ]; 61 | 62 | // Call the OpenAI API 63 | const completion = await openai.chat.completions.create({ 64 | model: this.model, 65 | messages, 66 | temperature: 0.2, // Lower temperature for more focused outputs 67 | }); 68 | 69 | // Get the generated content 70 | const content = completion.choices?.[0]?.message?.content || ''; 71 | 72 | // Try to parse content as JSON if it seems to be JSON 73 | if (content.trim().startsWith('{') && content.trim().endsWith('}')) { 74 | try { 75 | return JSON.parse(content); 76 | } catch (error) { 77 | // If parsing fails, return the content as output 78 | return { output: content }; 79 | } 80 | } 81 | 82 | // Default to returning content as output 83 | return { output: content }; 84 | } catch (error) { 85 | console.error(`[${this.name}] Error during execution:`, error); 86 | // Return a minimal response in case of error 87 | return { 88 | output: `Error occurred: ${error}`, 89 | error: true 90 | }; 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Mastra framework for orchestrating agents 97 | */ 98 | export class Mastra { 99 | agents: Record; 100 | 101 | constructor(config: { agents: Record }) { 102 | this.agents = config.agents; 103 | } 104 | 105 | /** 106 | * Get an agent by name 107 | * 108 | * @param name Agent name 109 | * @returns The agent instance 110 | */ 111 | getAgent(name: string): Agent { 112 | return this.agents[name]; 113 | } 114 | } 115 | 116 | /** 117 | * Create a tool for agent use 118 | * 119 | * @param config Tool configuration 120 | * @returns Tool definition 121 | */ 122 | export function createTool(config: any): any { 123 | return { ...config }; 124 | } 125 | 126 | /** 127 | * Create a vector query tool 128 | * 129 | * @param config Vector query tool configuration 130 | * @returns Vector query tool 131 | */ 132 | export function createVectorQueryTool(config: any): any { 133 | return { ...config, type: 'vector_query' }; 134 | } 135 | 136 | /** 137 | * OpenAI model provider 138 | * 139 | * @param model Model name 140 | * @returns Model identifier 141 | */ 142 | export function openai(model: string): string { 143 | return `openai:${model}`; 144 | } 145 | 146 | /** 147 | * Anthropic model provider 148 | * 149 | * @param model Model name 150 | * @returns Model identifier 151 | */ 152 | export function anthropic(model: string): string { 153 | return `anthropic:${model}`; 154 | } 155 | 156 | /** 157 | * Gemini model provider 158 | * 159 | * @param model Model name 160 | * @returns Model identifier 161 | */ 162 | export function gemini(model: string): string { 163 | return `gemini:${model}`; 164 | } 165 | 166 | /** 167 | * Groq model provider 168 | * 169 | * @param model Model name 170 | * @returns Model identifier 171 | */ 172 | export function groq(model: string): string { 173 | return `groq:${model}`; 174 | } 175 | 176 | /** 177 | * Mistral model provider 178 | * 179 | * @param model Model name 180 | * @returns Model identifier 181 | */ 182 | export function mistral(model: string): string { 183 | return `mistral:${model}`; 184 | } 185 | 186 | /** 187 | * GitHub Copilot model provider 188 | * 189 | * @param model Model name 190 | * @returns Model identifier 191 | */ 192 | export function copilot(model: string): string { 193 | return `copilot:${model}`; 194 | } 195 | 196 | /** 197 | * Ollama model provider 198 | * 199 | * @param model Model name 200 | * @returns Model identifier 201 | */ 202 | export function ollama(model: string): string { 203 | return `ollama:${model}`; 204 | } 205 | -------------------------------------------------------------------------------- /src/services/ai/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AI Models Management 3 | * 4 | * This module provides utilities for managing AI models from different providers. 5 | */ 6 | 7 | export const MODEL_PREFIXES = { 8 | ANTHROPIC: "claude-", 9 | OPENAI: "gpt-", 10 | OPENAI_O: "openai-o", 11 | GEMINI: "gemini-", 12 | GROQ: "groq-", 13 | MISTRAL: "mistral-", 14 | COPILOT: "copilot-", 15 | OLLAMA: "ollama-" 16 | } 17 | 18 | export const AVAILABLE_MODELS = [ 19 | // OpenAI models 20 | { id: "gpt-4o-mini", realId: "gpt-4o-mini" }, 21 | { id: "gpt-4o", realId: "gpt-4o" }, 22 | { id: "gpt-4.1", realId: "gpt-4.1" }, 23 | { id: "gpt-4.1-mini", realId: "gpt-4.1-mini" }, 24 | { id: "gpt-4.1-nano", realId: "gpt-4.1-nano" }, 25 | { id: "gpt-3.5-turbo", realId: "gpt-3.5-turbo" }, 26 | // OpenAI "o" models 27 | { id: "openai-o1", realId: "o1" }, 28 | { id: "openai-o1-mini", realId: "o1-mini" }, 29 | { id: "openai-o1-preview", realId: "o1-preview" }, 30 | { id: "openai-o3-mini", realId: "o3-mini" }, 31 | { id: "openai-o3", realId: "o3" }, 32 | { id: "openai-o4-mini", realId: "o4-mini" }, 33 | // Claude models 34 | { id: "claude-3-7-sonnet", realId: "claude-3-7-sonnet-20240307" }, 35 | { id: "claude-3-5-sonnet", realId: "claude-3-5-sonnet-20240620" }, 36 | { id: "claude-3-5-haiku", realId: "claude-3-5-haiku-20240307" }, 37 | { id: "claude-3-opus", realId: "claude-3-opus-20240229" }, 38 | // Gemini models 39 | { id: "gemini-2.5-flash", realId: "gemini-2.5-flash-preview-04-17" }, 40 | { id: "gemini-2.5-pro", realId: "gemini-2.5-pro-preview-05-06" }, 41 | { id: "gemini-2.0-flash", realId: "gemini-2.0-flash" }, 42 | { id: "gemini-2.0-flash-lite", realId: "gemini-2.0-flash-lite" }, 43 | { id: "gemini-1.5-flash", realId: "gemini-1.5-flash-latest" }, 44 | { id: "gemini-1.5-pro", realId: "gemini-1.5-pro-latest" }, 45 | // Groq models 46 | { id: "groq-llama-3.3-70b", realId: "llama-3.3-70b-versatile" }, 47 | { id: "groq-llama-3.1-8b", realId: "llama-3.1-8b-instant" }, 48 | { id: "groq-mixtral-8x7b", realId: "mixtral-8x7b-32768" }, 49 | // Mistral models 50 | { id: "mistral-large", realId: "mistral-large-latest" }, 51 | { id: "mistral-medium", realId: "mistral-medium-latest" }, 52 | { id: "mistral-small", realId: "mistral-small-latest" }, 53 | // GitHub Copilot models 54 | { id: "copilot-gpt-4o", realId: "gpt-4o" }, 55 | { id: "copilot-o1-mini", realId: "o1-mini" }, 56 | { id: "copilot-o1-preview", realId: "o1-preview" }, 57 | { id: "copilot-claude-3.5-sonnet", realId: "claude-3.5-sonnet" }, 58 | ] 59 | 60 | /** 61 | * Get all available models 62 | * 63 | * @param includeAll Whether to include all models or just the main ones 64 | * @param includeOllama Whether to include Ollama local models 65 | * @returns Array of model objects 66 | */ 67 | export async function getAllModels(includeAll = false, includeOllama = false) { 68 | const models = [...AVAILABLE_MODELS]; 69 | 70 | if (includeAll) { 71 | return models; 72 | } 73 | 74 | if (includeOllama) { 75 | try { 76 | // Fetch available Ollama models from the local server 77 | const host = process.env.OLLAMA_HOST || "http://localhost:11434"; 78 | const response = await fetch(`${host}/api/tags`); 79 | 80 | if (response.ok) { 81 | const data = await response.json(); 82 | if (data.models && Array.isArray(data.models)) { 83 | // Add each Ollama model to the list 84 | data.models.forEach((model: any) => { 85 | if (model.name) { 86 | models.push({ 87 | id: `ollama-${model.name}`, 88 | realId: model.name 89 | }); 90 | } 91 | }); 92 | 93 | console.log(`Found ${data.models.length} local Ollama models`); 94 | } 95 | } else { 96 | console.warn("Failed to connect to Ollama server - is it running?"); 97 | // Add a default model as a fallback 98 | models.push({ id: "ollama-llama3", realId: "llama3" }); 99 | } 100 | } catch (error) { 101 | console.warn("Failed to fetch Ollama models:", error); 102 | // Add a default model as a fallback 103 | models.push({ id: "ollama-llama3", realId: "llama3" }); 104 | } 105 | } 106 | 107 | return models; 108 | } 109 | 110 | /** 111 | * Get a cheaper model ID based on a model ID 112 | * 113 | * @param modelId The model ID 114 | * @returns A cheaper model ID 115 | */ 116 | export function getCheapModelId(modelId: string) { 117 | if (modelId.startsWith(MODEL_PREFIXES.ANTHROPIC)) { 118 | return "claude-3-5-haiku"; 119 | } 120 | 121 | if (modelId.startsWith(MODEL_PREFIXES.GEMINI)) { 122 | return "gemini-1.5-flash"; 123 | } 124 | 125 | if (modelId.startsWith(MODEL_PREFIXES.GROQ)) { 126 | return "groq-llama-3.1-8b"; 127 | } 128 | 129 | if (modelId.startsWith(MODEL_PREFIXES.MISTRAL)) { 130 | return "mistral-small"; 131 | } 132 | 133 | if (modelId.startsWith(MODEL_PREFIXES.COPILOT)) { 134 | return "copilot-o1-mini"; 135 | } 136 | 137 | if (modelId.startsWith(MODEL_PREFIXES.OPENAI_O)) { 138 | return "openai-o1-mini"; 139 | } 140 | 141 | return "gpt-4o-mini"; 142 | } 143 | 144 | /** 145 | * Get the provider name from a model ID 146 | * 147 | * @param modelId The model ID 148 | * @returns The provider name 149 | */ 150 | export function getModelProvider(modelId: string) { 151 | if (modelId.startsWith(MODEL_PREFIXES.ANTHROPIC)) { 152 | return "anthropic"; 153 | } 154 | 155 | if (modelId.startsWith(MODEL_PREFIXES.GEMINI)) { 156 | return "gemini"; 157 | } 158 | 159 | if (modelId.startsWith(MODEL_PREFIXES.GROQ)) { 160 | return "groq"; 161 | } 162 | 163 | if (modelId.startsWith(MODEL_PREFIXES.MISTRAL)) { 164 | return "mistral"; 165 | } 166 | 167 | if (modelId.startsWith(MODEL_PREFIXES.COPILOT)) { 168 | return "copilot"; 169 | } 170 | 171 | if (modelId.startsWith(MODEL_PREFIXES.OLLAMA)) { 172 | return "ollama"; 173 | } 174 | 175 | // Default to OpenAI for both gpt- and openai-o prefixes 176 | return "openai"; 177 | } 178 | 179 | /** 180 | * Get the real model ID that should be used with the provider's API 181 | * 182 | * @param modelId The model ID used in our system 183 | * @returns The real model ID to use with the provider's API 184 | */ 185 | export function getRealModelId(modelId: string) { 186 | const model = AVAILABLE_MODELS.find(m => m.id === modelId); 187 | return model?.realId || modelId; 188 | } -------------------------------------------------------------------------------- /src/services/ai/rag-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieval Augmented Generation (RAG) Utilities 3 | * 4 | * This module provides utilities for integrating the vector database 5 | * with AI queries for retrieval-augmented generation. 6 | */ 7 | import { VectorDB } from '../vector-db/vector-db.js'; 8 | import { loadConfig } from '../config/config.js'; 9 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; 10 | loadConfig(); 11 | /** 12 | * Get relevant content from a collection for a query 13 | * 14 | * @param query The query to search for 15 | * @param collectionName The collection to search in 16 | * @param k Number of results to retrieve 17 | * @returns Formatted context string 18 | */ 19 | export async function getRelevantContent( 20 | query: string, 21 | collectionName: string, 22 | k: number = 5 23 | ): Promise { 24 | const db = new VectorDB(); 25 | 26 | try { 27 | const results = await db.search(collectionName, query, k); 28 | 29 | if (results.length === 0) { 30 | return ''; 31 | } 32 | 33 | // Format results into a single context string 34 | let context = `## Relevant information from ${collectionName} documentation:\n\n`; 35 | 36 | results.forEach((doc: any) => { 37 | const source = doc.metadata?.source || 'Unknown source'; 38 | context += `### Source: ${source}\n\n${doc.pageContent}\n\n---\n\n`; 39 | }); 40 | 41 | return context; 42 | } catch (error) { 43 | console.error(`Error retrieving content from collection '${collectionName}':`, error); 44 | return ''; 45 | } 46 | } 47 | 48 | /** 49 | * Augment messages with relevant content from vector database 50 | * 51 | * @param messages Array of chat messages 52 | * @param query Search query 53 | * @param collectionNames Collections to search in 54 | * @returns Augmented messages array 55 | */ 56 | export async function augmentMessagesWithRAG( 57 | messages: ChatCompletionMessageParam[], 58 | query: string, 59 | collectionNames: string[] | string 60 | ): Promise { 61 | // Convert single collection name to array 62 | const collections = Array.isArray(collectionNames) ? collectionNames : [collectionNames]; 63 | 64 | // Get relevant content from each collection 65 | const contextPromises = collections.map(name => getRelevantContent(query, name)); 66 | const contextResults = await Promise.all(contextPromises); 67 | 68 | // Filter out empty results and combine 69 | const combinedContext = contextResults.filter(Boolean).join('\n\n'); 70 | 71 | if (!combinedContext) { 72 | return messages; 73 | } 74 | 75 | // Create a new message array with context inserted before the user query 76 | const augmentedMessages: ChatCompletionMessageParam[] = [...messages]; 77 | 78 | // Find the last user message 79 | const lastUserMsgIndex = augmentedMessages.findIndex( 80 | (msg, i, arr) => msg.role === 'user' && (i === arr.length - 1 || arr[i + 1].role !== 'user') 81 | ); 82 | 83 | if (lastUserMsgIndex !== -1) { 84 | // If we found a user message, augment it with context 85 | const userMsg = augmentedMessages[lastUserMsgIndex]; 86 | const userContent = typeof userMsg.content === 'string' ? userMsg.content : ''; 87 | 88 | augmentedMessages[lastUserMsgIndex] = { 89 | ...userMsg, 90 | content: `${userContent}\n\nHere's some relevant information to help you answer:\n\n${combinedContext}` 91 | }; 92 | } 93 | 94 | return augmentedMessages; 95 | } 96 | 97 | /** 98 | * Process a vector database reading request 99 | * 100 | * @param query The query to process 101 | * @param collectionName The collection name to read from 102 | * @param k Number of results to retrieve 103 | * @returns Message string with retrieved content 104 | */ 105 | export async function processVectorDBReadRequest( 106 | query: string, 107 | collectionName: string, 108 | k: number = 5 109 | ): Promise { 110 | try { 111 | const content = await getRelevantContent(query, collectionName, k); 112 | 113 | if (!content) { 114 | return `No relevant information found in collection '${collectionName}' for query: ${query}`; 115 | } 116 | 117 | return content; 118 | } catch (error: any) { 119 | return `Error retrieving information: ${error.message || error}`; 120 | } 121 | } -------------------------------------------------------------------------------- /src/services/config/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration Management Service 3 | * 4 | * This module provides functionality for loading and managing configuration 5 | * from various sources (environment, config files, etc.) 6 | */ 7 | import JoyCon from "joycon"; 8 | import fs from "node:fs"; 9 | import os from "node:os"; 10 | import path from "node:path"; 11 | import toml from "smol-toml"; 12 | import { z } from "zod"; 13 | 14 | /** 15 | * Path to the configuration directory 16 | */ 17 | export const configDirPath = path.join(os.homedir(), ".config", "web3cli"); 18 | 19 | /** 20 | * Schema for AI command variables 21 | */ 22 | const AICommandVariableSchema = z.union([ 23 | z.string().describe("a shell command to run"), 24 | z 25 | .object({ 26 | type: z.literal("input"), 27 | message: z.string(), 28 | }) 29 | .describe("get text input from the user"), 30 | z 31 | .object({ 32 | type: z.literal("select"), 33 | message: z.string(), 34 | choices: z.array( 35 | z.object({ 36 | value: z.string(), 37 | title: z.string(), 38 | }) 39 | ), 40 | }) 41 | .describe("get a choice from the user"), 42 | ]); 43 | 44 | export type AICommandVariable = z.infer; 45 | 46 | /** 47 | * Schema for AI commands 48 | */ 49 | const AICommandSchema = z.object({ 50 | command: z.string().describe("the cli command"), 51 | example: z.string().optional().describe("example to show in cli help"), 52 | description: z 53 | .string() 54 | .optional() 55 | .describe("description to show in cli help"), 56 | variables: z.record(AICommandVariableSchema).optional(), 57 | prompt: z.string().describe("the prompt to send to the model"), 58 | require_stdin: z 59 | .boolean() 60 | .optional() 61 | .describe("Require piping output from another program to Web3CLI"), 62 | }); 63 | 64 | export type AICommand = z.infer; 65 | 66 | /** 67 | * Schema for the configuration file 68 | */ 69 | export const ConfigSchema = z.object({ 70 | default_model: z.string().optional(), 71 | openai_api_key: z 72 | .string() 73 | .optional() 74 | .describe('Default to the "OPENAI_API_KEY" environment variable'), 75 | openai_api_url: z 76 | .string() 77 | .optional() 78 | .describe('Default to the "OPENAI_API_URL" environment variable'), 79 | anthropic_api_key: z 80 | .string() 81 | .optional() 82 | .describe('Default to the "ANTHROPIC_API_KEY" environment variable'), 83 | gemini_api_key: z 84 | .string() 85 | .optional() 86 | .describe('Default to the "GEMINI_API_KEY" environment variable'), 87 | gemini_api_url: z 88 | .string() 89 | .optional() 90 | .describe('Default to the "GEMINI_API_URL" environment variable'), 91 | groq_api_key: z 92 | .string() 93 | .optional() 94 | .describe('Default to the "GROQ_API_KEY" environment variable'), 95 | groq_api_url: z 96 | .string() 97 | .optional() 98 | .describe('Default to the "GROQ_API_URL" environment variable'), 99 | mistral_api_key: z 100 | .string() 101 | .optional() 102 | .describe('Default to the "MISTRAL_API_KEY" environment variable'), 103 | mistral_api_url: z 104 | .string() 105 | .optional() 106 | .describe('Default to the "MISTRAL_API_URL" environment variable'), 107 | etherscan_api_key: z 108 | .string() 109 | .optional() 110 | .describe('Default to the "ETHERSCAN_API_KEY" environment variable'), 111 | ollama_host: z 112 | .string() 113 | .optional() 114 | .describe('Default to the "OLLAMA_HOST" environment variable'), 115 | commands: z.array(AICommandSchema).optional(), 116 | }); 117 | 118 | export type Config = z.infer; 119 | 120 | /** 121 | * Load configuration from various sources 122 | * 123 | * @returns Merged configuration 124 | */ 125 | export function loadConfig(): Config { 126 | const joycon = new JoyCon.default(); 127 | 128 | joycon.addLoader({ 129 | test: /\.toml$/, 130 | loadSync: (filepath: string) => { 131 | const content = fs.readFileSync(filepath, "utf-8"); 132 | return toml.parse(content); 133 | }, 134 | }); 135 | 136 | function safeLoad(filenames: string[], cwd: string, stopDir: string): Config | undefined { 137 | try { 138 | const result = joycon.loadSync(filenames, cwd, stopDir); 139 | return result.data as Config | undefined; 140 | } catch (err) { 141 | const message = err instanceof Error ? err.message : String(err); 142 | console.warn( 143 | `Warning: ignored malformed config while reading ${filenames.join(", ")} — ${message}` 144 | ); 145 | return undefined; 146 | } 147 | } 148 | 149 | const globalConfig = safeLoad( 150 | ["web3cli.json", "web3cli.toml"], 151 | configDirPath, 152 | path.dirname(configDirPath) 153 | ); 154 | 155 | const localConfig = safeLoad( 156 | ["web3cli.json", "web3cli.toml"], 157 | process.cwd(), 158 | path.dirname(process.cwd()) 159 | ); 160 | 161 | let baseConfig: Config | undefined = undefined; 162 | let commandsFromConfig: AICommand[] = []; 163 | 164 | if (globalConfig) { 165 | baseConfig = { ...globalConfig }; 166 | commandsFromConfig = [...(globalConfig.commands || [])]; 167 | } else if (localConfig) { 168 | baseConfig = { ...localConfig }; 169 | commandsFromConfig = [...(localConfig.commands || [])]; 170 | } else { 171 | baseConfig = {}; // Ensure baseConfig is always an object 172 | } 173 | 174 | const config: Config = { 175 | ...(baseConfig as Config), // Spread, ensuring it's treated as Config 176 | commands: commandsFromConfig, 177 | }; 178 | 179 | const envVarMapping = { 180 | openai_api_key: "OPENAI_API_KEY", 181 | openai_api_url: "OPENAI_API_URL", 182 | anthropic_api_key: "ANTHROPIC_API_KEY", 183 | gemini_api_key: "GEMINI_API_KEY", 184 | gemini_api_url: "GEMINI_API_URL", 185 | groq_api_key: "GROQ_API_KEY", 186 | groq_api_url: "GROQ_API_URL", 187 | mistral_api_key: "MISTRAL_API_KEY", 188 | mistral_api_url: "MISTRAL_API_URL", 189 | etherscan_api_key: "ETHERSCAN_API_KEY", 190 | ollama_host: "OLLAMA_HOST", 191 | }; 192 | 193 | for (const [configKey, envVar] of Object.entries(envVarMapping)) { 194 | if (process.env[envVar] && !(config as any)[configKey]) { 195 | (config as any)[configKey] = process.env[envVar]; 196 | } 197 | } 198 | 199 | return config; 200 | } -------------------------------------------------------------------------------- /src/services/contract/contract-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract Utilities 3 | * 4 | * This module provides utilities for working with smart contracts, 5 | * including fetching contract data from Etherscan. 6 | */ 7 | import axios from 'axios'; 8 | import fs from 'node:fs'; 9 | import path from 'node:path'; 10 | import { loadConfig, configDirPath } from '../config/config.js'; 11 | import { ethers } from 'ethers'; 12 | 13 | /** 14 | * The result of fetching contract source code from Etherscan 15 | */ 16 | export interface ContractSourceResult { 17 | abi: any; 18 | contractName: string; 19 | sourceCode: string; 20 | bytecode: string; 21 | constructorArguments: string; 22 | compilerVersion: string; 23 | optimizationUsed: string; 24 | runs: string; 25 | evmVersion: string; 26 | library: string; 27 | licenseType: string; 28 | proxy: string; 29 | implementation: string; 30 | swarmSource: string; 31 | } 32 | 33 | /** 34 | * Network configuration for supported networks 35 | */ 36 | export interface NetworkConfig { 37 | name: string; 38 | chainId: number; 39 | apiUrl: string; 40 | explorerUrl: string; 41 | } 42 | 43 | /** 44 | * Networks supported by Etherscan API 45 | */ 46 | export const SUPPORTED_NETWORKS: Record = { 47 | mainnet: { 48 | name: 'Ethereum Mainnet', 49 | chainId: 1, 50 | apiUrl: 'https://api.etherscan.io/api', 51 | explorerUrl: 'https://etherscan.io' 52 | }, 53 | sepolia: { 54 | name: 'Sepolia Testnet', 55 | chainId: 11155111, 56 | apiUrl: 'https://api-sepolia.etherscan.io/api', 57 | explorerUrl: 'https://sepolia.etherscan.io' 58 | }, 59 | goerli: { 60 | name: 'Goerli Testnet', 61 | chainId: 5, 62 | apiUrl: 'https://api-goerli.etherscan.io/api', 63 | explorerUrl: 'https://goerli.etherscan.io' 64 | }, 65 | polygon: { 66 | name: 'Polygon Mainnet', 67 | chainId: 137, 68 | apiUrl: 'https://api.polygonscan.com/api', 69 | explorerUrl: 'https://polygonscan.com' 70 | }, 71 | arbitrum: { 72 | name: 'Arbitrum One', 73 | chainId: 42161, 74 | apiUrl: 'https://api.arbiscan.io/api', 75 | explorerUrl: 'https://arbiscan.io' 76 | }, 77 | optimism: { 78 | name: 'Optimism Mainnet', 79 | chainId: 10, 80 | apiUrl: 'https://api-optimistic.etherscan.io/api', 81 | explorerUrl: 'https://optimistic.etherscan.io' 82 | } 83 | }; 84 | 85 | /** 86 | * Get network configuration for the given network name 87 | * 88 | * @param network Network name (e.g., 'mainnet', 'sepolia') 89 | * @returns Network configuration 90 | */ 91 | export function getNetworkConfig(network: string): NetworkConfig { 92 | const config = SUPPORTED_NETWORKS[network.toLowerCase()]; 93 | 94 | if (!config) { 95 | throw new Error(`Unsupported network: ${network}. Supported networks: ${Object.keys(SUPPORTED_NETWORKS).join(', ')}`); 96 | } 97 | 98 | return config; 99 | } 100 | 101 | /** 102 | * Fetch contract source code from Etherscan 103 | * 104 | * @param contractAddress Contract address 105 | * @param networkName Network name (default: 'sepolia') 106 | * @returns Contract source code information 107 | */ 108 | export async function fetchContractSource( 109 | contractAddress: string, 110 | networkName: string = 'sepolia' 111 | ): Promise { 112 | const config = loadConfig(); 113 | const apiKey = config.etherscan_api_key; 114 | 115 | if (!apiKey) { 116 | const localPath = path.join(process.cwd(), 'web3cli.toml'); 117 | const globalPath = path.join(configDirPath, 'web3cli.toml'); 118 | throw new Error( 119 | `Etherscan API key not found. Please set the ETHERSCAN_API_KEY environment variable, ` + 120 | `or add etherscan_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` 121 | ); 122 | } 123 | 124 | const network = getNetworkConfig(networkName); 125 | 126 | const response = await axios.get(network.apiUrl, { 127 | params: { 128 | module: 'contract', 129 | action: 'getsourcecode', 130 | address: contractAddress, 131 | apikey: apiKey 132 | } 133 | }); 134 | 135 | if (response.data.status !== '1') { 136 | throw new Error(`Etherscan API error: ${response.data.message}`); 137 | } 138 | 139 | const sourceData = response.data.result[0]; 140 | 141 | if (!sourceData) { 142 | throw new Error(`Contract not found at address ${contractAddress}`); 143 | } 144 | 145 | return { 146 | abi: sourceData.ABI !== 'Contract source code not verified' ? JSON.parse(sourceData.ABI) : null, 147 | contractName: sourceData.ContractName, 148 | sourceCode: sourceData.SourceCode, 149 | bytecode: sourceData.ByteCode, 150 | constructorArguments: sourceData.ConstructorArguments, 151 | compilerVersion: sourceData.CompilerVersion, 152 | optimizationUsed: sourceData.OptimizationUsed, 153 | runs: sourceData.Runs, 154 | evmVersion: sourceData.EVMVersion, 155 | library: sourceData.Library, 156 | licenseType: sourceData.LicenseType, 157 | proxy: sourceData.Proxy, 158 | implementation: sourceData.Implementation, 159 | swarmSource: sourceData.SwarmSource 160 | }; 161 | } 162 | 163 | /** 164 | * Create an ethers.js contract instance 165 | * 166 | * @param address Contract address 167 | * @param abi Contract ABI 168 | * @param network Network name (default: 'sepolia') 169 | * @returns Ethers.js contract instance 170 | */ 171 | export function createContractInstance( 172 | address: string, 173 | abi: any, 174 | network: string = 'sepolia' 175 | ): ethers.Contract { 176 | // Create a provider for the specified network 177 | getNetworkConfig(network); 178 | const provider = new ethers.JsonRpcProvider(`https://${network}.infura.io/v3/your-infura-key`); 179 | 180 | // Create a contract instance 181 | return new ethers.Contract(address, abi, provider); 182 | } 183 | 184 | /** 185 | * Save contract data to files 186 | * 187 | * @param outputPath The base path to save files to 188 | * @param contractData Contract data 189 | * @param address Contract address 190 | * @returns Paths to saved files 191 | */ 192 | export function saveContractData( 193 | outputPath: string, 194 | contractData: ContractSourceResult, 195 | address: string 196 | ): { sourcePath: string; abiPath: string; infoPath: string } { 197 | // Create output directory if it doesn't exist 198 | const outputDir = path.dirname(outputPath); 199 | fs.mkdirSync(outputDir, { recursive: true }); 200 | 201 | // Determine filenames 202 | const baseName = path.basename(outputPath, '.sol'); 203 | const sourcePath = path.join(outputDir, `${baseName}.sol`); 204 | const abiPath = path.join(outputDir, `${baseName}.abi.json`); 205 | const infoPath = path.join(outputDir, `${baseName}.info.json`); 206 | 207 | // Save source code 208 | fs.writeFileSync(sourcePath, contractData.sourceCode); 209 | 210 | // Save ABI 211 | if (contractData.abi) { 212 | fs.writeFileSync(abiPath, JSON.stringify(contractData.abi, null, 2)); 213 | } 214 | 215 | // Save contract metadata 216 | const contractInfo = { 217 | address, 218 | name: contractData.contractName, 219 | compiler: contractData.compilerVersion, 220 | optimization: contractData.optimizationUsed, 221 | runs: contractData.runs, 222 | evmVersion: contractData.evmVersion, 223 | license: contractData.licenseType, 224 | isProxy: contractData.proxy === '1', 225 | implementation: contractData.implementation 226 | }; 227 | 228 | fs.writeFileSync(infoPath, JSON.stringify(contractInfo, null, 2)); 229 | 230 | return { 231 | sourcePath, 232 | abiPath, 233 | infoPath 234 | }; 235 | } -------------------------------------------------------------------------------- /src/services/contract/explain-contract.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract Explanation Service 3 | * 4 | * This module provides functionality for explaining smart contracts, 5 | * including their purpose, functions, and potential security issues. 6 | */ 7 | import fs from "node:fs"; 8 | import { getSDKModel } from "../ai/ai-sdk.js"; 9 | import { loadConfig } from "../config/config.js"; 10 | import { CliError } from "../../utils/error.js"; 11 | import logUpdate from "log-update"; 12 | import { renderMarkdown } from "../../utils/markdown.js"; 13 | import { VectorDB } from "../vector-db/vector-db.js"; 14 | import { CoreMessage } from "ai"; 15 | import { notEmpty } from "../../utils/common.js"; 16 | 17 | /** 18 | * Explain a smart contract by address or file path 19 | * 20 | * @param source Contract address or file path 21 | * @param options Explanation options 22 | */ 23 | export async function explainContract( 24 | source: string, 25 | options: { 26 | model?: string; 27 | network?: string; 28 | stream?: boolean; 29 | readDocs?: string; 30 | } = {} 31 | ) { 32 | // Check if source is a file path or contract address 33 | const isFile = source.endsWith(".sol") || fs.existsSync(source); 34 | 35 | console.log(`Explaining ${isFile ? "contract file" : "deployed contract"}...`); 36 | 37 | // Load contract content 38 | let contractContent = ""; 39 | if (isFile) { 40 | try { 41 | contractContent = fs.readFileSync(source, "utf8"); 42 | } catch (error) { 43 | throw new CliError(`Failed to read contract file: ${error}`); 44 | } 45 | } else { 46 | // In a real implementation, this would fetch the contract bytecode and ABI from the blockchain 47 | // For this example, we'll use a placeholder 48 | contractContent = `// Contract would be fetched from ${options.network} network at address ${source}`; 49 | } 50 | 51 | const config = loadConfig(); 52 | const modelId = options.model || config.default_model || "gpt-4o-mini"; 53 | const openai = await getSDKModel(modelId, config); 54 | 55 | // Handle vector DB docs 56 | let docsContext: string[] = []; 57 | if (options.readDocs) { 58 | try { 59 | const vdb = new VectorDB(); 60 | const docs = await vdb.similaritySearch(options.readDocs, contractContent.substring(0, 500), 5); 61 | if (docs.length > 0) { 62 | docsContext = [ 63 | `docs:${options.readDocs}:`, 64 | ...docs.map((d: any) => `""" 65 | ${d.text} 66 | """`), 67 | ]; 68 | } 69 | } catch (e) { 70 | // ignore if vector db fails 71 | console.warn("Warning: Could not retrieve docs from vector DB:", e); 72 | } 73 | } 74 | 75 | const context = [ 76 | `platform: ${process.platform}`, 77 | 78 | `contract:`, 79 | `""" 80 | ${contractContent} 81 | """`, 82 | 83 | ...docsContext, 84 | ] 85 | .filter(notEmpty) 86 | .join("\n"); 87 | 88 | const messages: CoreMessage[] = [ 89 | { 90 | role: "system", 91 | content: `You are an expert Solidity developer who specializes in analyzing and explaining smart contracts. 92 | 93 | Provide a comprehensive explanation of the contract that includes: 94 | 1. Overall purpose and functionality 95 | 2. Key functions and their purposes 96 | 3. State variables and data structures 97 | 4. Access control mechanisms 98 | 5. Events and when they're emitted 99 | 6. Potential security concerns or vulnerabilities 100 | 7. Gas efficiency considerations 101 | 8. Best practices followed or violated 102 | 103 | Structure your response with clear headings and bullet points where appropriate. 104 | `, 105 | }, 106 | { 107 | role: "user", 108 | content: [ 109 | context && `CONTEXT:\n${context}`, 110 | `TASK: Explain the following smart contract in detail:`, 111 | ] 112 | .filter(Boolean) 113 | .join("\n\n"), 114 | }, 115 | ]; 116 | 117 | try { 118 | let content = ""; 119 | 120 | if (options.stream !== false) { 121 | const stream = await openai.chat.completions.create({ 122 | model: modelId, 123 | messages: [ 124 | { role: "system", content: messages[0].content as string }, 125 | { role: "user", content: messages[1].content as string } 126 | ], 127 | stream: true, 128 | }); 129 | 130 | for await (const chunk of stream as any) { 131 | const content_chunk = chunk.choices[0]?.delta?.content || ""; 132 | content += content_chunk; 133 | logUpdate(renderMarkdown(content)); 134 | } 135 | logUpdate.done(); 136 | } else { 137 | const completion = await openai.chat.completions.create({ 138 | model: modelId, 139 | messages: [ 140 | { role: "system", content: messages[0].content as string }, 141 | { role: "user", content: messages[1].content as string } 142 | ], 143 | }); 144 | content = completion.choices?.[0]?.message?.content || ""; 145 | console.log(renderMarkdown(content)); 146 | } 147 | 148 | return { explanation: content }; 149 | } catch (error) { 150 | console.error("Error explaining contract:", error); 151 | throw error; 152 | } 153 | } -------------------------------------------------------------------------------- /src/services/contract/generate-contract.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contract Generation Service 3 | * 4 | * This module provides functionality for generating smart contracts from 5 | * natural language descriptions. 6 | */ 7 | import fs from "node:fs"; 8 | import path from "node:path"; 9 | import { CoreMessage } from "ai"; 10 | import { getSDKModel } from "../ai/ai-sdk.js"; 11 | import { loadConfig } from "../config/config.js"; 12 | import { loadFiles, notEmpty } from "../../utils/common.js"; 13 | import { fetchUrl } from "../../utils/fetch-url.js"; 14 | import { VectorDB } from "../vector-db/vector-db.js"; 15 | import { CliError } from "../../utils/error.js"; 16 | import logUpdate from "log-update"; 17 | import { renderMarkdown, stripMarkdownCodeBlocks } from "../../utils/markdown.js"; 18 | import { ChatCompletionMessageParam } from "openai/resources/chat/completions"; 19 | 20 | /** 21 | * Generate a smart contract from natural language 22 | * 23 | * @param prompt The natural language prompt 24 | * @param options Generation options 25 | */ 26 | export async function generateContract( 27 | prompt: string, 28 | options: { 29 | model?: string; 30 | files?: string | string[]; 31 | url?: string | string[]; 32 | search?: boolean; 33 | stream?: boolean; 34 | output?: string; 35 | hardhat?: boolean; 36 | pipeInput?: string; 37 | readDocs?: string; 38 | proxy?: 'transparent' | 'uups'; 39 | } = {} 40 | ) { 41 | if (!prompt) { 42 | throw new CliError("Please provide a prompt describing the smart contract"); 43 | } 44 | 45 | console.log("Generating smart contract..."); 46 | 47 | const config = loadConfig(); 48 | const modelId = options.model || config.default_model || "gpt-4o-mini"; 49 | const openai = await getSDKModel(modelId, config); 50 | 51 | const files = await loadFiles(options.files || []); 52 | const remoteContents = await fetchUrl(options.url || []); 53 | 54 | // Handle vector DB docs 55 | let docsContext: string[] = []; 56 | if (options.readDocs) { 57 | try { 58 | const vdb = new VectorDB(); 59 | const docs = await vdb.similaritySearch(options.readDocs, prompt, 8); 60 | if (docs.length > 0) { 61 | docsContext = [ 62 | `docs:${options.readDocs}:`, 63 | ...docs.map((d: { text?: string, pageContent?: string }) => `""" 64 | ${d.text || d.pageContent || ''} 65 | """`), 66 | ]; 67 | } 68 | } catch (e) { 69 | // ignore if vector db fails 70 | console.warn("Warning: Could not retrieve docs from vector DB:", e); 71 | } 72 | } 73 | 74 | const context = [ 75 | `platform: ${process.platform}\nsolidity: ^0.8.20`, 76 | 77 | options.pipeInput && [`stdin:`, "```", options.pipeInput, "```"].join("\n"), 78 | 79 | files.length > 0 && "files:", 80 | ...files.map((file: any) => `${file.name}:\n"""\n${file.content}\n"""`), 81 | 82 | remoteContents.length > 0 && "remote contents:", 83 | ...remoteContents.map( 84 | (content) => `${content.url}: 85 | """ 86 | ${content.content} 87 | """` 88 | ), 89 | ...docsContext, 90 | ] 91 | .filter(notEmpty) 92 | .join("\n"); 93 | 94 | // Determine proxy-specific guidelines 95 | let proxyGuideline = ''; 96 | if (options.proxy === 'transparent') { 97 | proxyGuideline = 'Additionally, implement upgradeability using the OpenZeppelin TransparentUpgradeableProxy pattern. Provide the implementation contract with an initializer (no constructor) and include the TransparentUpgradeableProxy deployment setup. Organize the output in a folder structure such as contracts/, proxy/, and scripts/.'; 98 | } else if (options.proxy === 'uups') { 99 | proxyGuideline = 'Additionally, implement upgradeability using the OpenZeppelin UUPS (Universal Upgradeable Proxy Standard) pattern. Ensure the implementation inherits from UUPSUpgradeable and has an initializer (no constructor). Organize the output in a folder structure such as contracts/, proxy/, and scripts/.'; 100 | } 101 | 102 | const messages: CoreMessage[] = [ 103 | { 104 | role: "system", 105 | content: `You are an expert Solidity developer who specializes in creating secure, efficient, and well-documented smart contracts. 106 | 107 | Output only valid Solidity code without additional explanations. The contract should: 108 | - Use the most recent Solidity version (^0.8.20) 109 | - Be secure, following all best practices 110 | - Use appropriate OpenZeppelin contracts when relevant 111 | - Include comprehensive NatSpec documentation 112 | - Be gas-efficient 113 | - Include appropriate events, modifiers, and access control 114 | 115 | ${options.hardhat ? "After the contract, include a Hardhat test file that thoroughly tests the contract functionality." : ""} 116 | ${proxyGuideline} 117 | `, 118 | }, 119 | { 120 | role: "user", 121 | content: [ 122 | context && `CONTEXT:\n${context}`, 123 | `TASK: Generate a Solidity smart contract for the following requirements:`, 124 | prompt, 125 | ] 126 | .filter(Boolean) 127 | .join("\n\n"), 128 | }, 129 | ]; 130 | 131 | try { 132 | let content = ""; 133 | 134 | if (options.stream !== false) { 135 | const stream = await openai.chat.completions.create({ 136 | model: modelId, 137 | messages: messages as ChatCompletionMessageParam[], 138 | stream: true, 139 | }); 140 | 141 | for await (const chunk of stream as any) { 142 | const content_chunk = chunk.choices[0]?.delta?.content || ""; 143 | content += content_chunk; 144 | logUpdate(renderMarkdown(content)); 145 | } 146 | logUpdate.done(); 147 | } else { 148 | const completion = await openai.chat.completions.create({ 149 | model: modelId, 150 | messages: messages as ChatCompletionMessageParam[], 151 | }); 152 | content = completion.choices?.[0]?.message?.content || ""; 153 | console.log(renderMarkdown(content)); 154 | } 155 | 156 | // Attempt to identify and save the contract 157 | if (options.output) { 158 | // Strip markdown formatting before saving 159 | const cleanContent = stripMarkdownCodeBlocks(content); 160 | 161 | // Create directory if it doesn't exist 162 | const outputDir = path.dirname(options.output); 163 | fs.mkdirSync(outputDir, { recursive: true }); 164 | 165 | fs.writeFileSync(options.output, cleanContent); 166 | console.log(`\n✅ Contract saved to ${options.output}`); 167 | 168 | // If there's a test file section, save it separately 169 | if (options.hardhat && content.includes("// Test file")) { 170 | const testParts = content.split(/\/\/ Test file/); 171 | if (testParts.length >= 2) { 172 | const testContent = testParts[1].trim(); 173 | // Strip markdown formatting from test code 174 | const cleanTestContent = stripMarkdownCodeBlocks(testContent); 175 | const testPath = options.output.replace(/\.sol$/, ".test.js"); 176 | fs.writeFileSync(testPath, cleanTestContent); 177 | console.log(`✅ Test file saved to ${testPath}`); 178 | } 179 | } 180 | } 181 | 182 | return { code: content }; 183 | } catch (error) { 184 | console.error("Error generating contract:", error); 185 | throw error; 186 | } 187 | } -------------------------------------------------------------------------------- /src/services/contract/generator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart Contract Generator Service 3 | * 4 | * This module provides functionality for generating smart contracts from 5 | * natural language descriptions. 6 | */ 7 | import fs from 'node:fs'; 8 | import path from 'node:path'; 9 | import { getAIClient } from '../ai/client.js'; 10 | import { getSearchResults } from '../search/search.js'; 11 | import { VectorDB } from '../vector-db/vector-db.js'; 12 | import { loadFiles } from '../../utils/common.js'; 13 | 14 | /** 15 | * Options for contract generation 16 | */ 17 | export interface GenerateOptions { 18 | model?: string; 19 | stream?: boolean; 20 | files?: string; 21 | url?: string; 22 | search?: boolean; 23 | readDocs?: string; 24 | output?: string; 25 | hardhat?: boolean; 26 | pipeInput?: string; 27 | proxy?: 'transparent' | 'uups'; 28 | } 29 | 30 | /** 31 | * Generate a smart contract from natural language 32 | * 33 | * @param prompt The natural language description 34 | * @param options Generation options 35 | * @returns Generated contract code and security notes 36 | */ 37 | export async function generateContract( 38 | prompt: string, 39 | options: GenerateOptions 40 | ): Promise<{ code: string; securityNotes: string; testCode?: string }> { 41 | console.log('➤ Generating smart contract…'); 42 | 43 | // Gather context from various sources 44 | let context = ''; 45 | 46 | // Add file context 47 | if (options.files) { 48 | context += await getFileContext(options.files); 49 | } 50 | 51 | // Add URL context 52 | if (options.url) { 53 | context += await getUrlContext(options.url); 54 | } 55 | 56 | // Add search context 57 | if (options.search) { 58 | context += await getSearchContext(prompt); 59 | } 60 | 61 | // Add vector database context 62 | if (options.readDocs) { 63 | context += await getVectorDBContext(options.readDocs, prompt); 64 | } 65 | 66 | // Add pipe input if available 67 | if (options.pipeInput) { 68 | context += `\nPipe Input:\n${options.pipeInput}`; 69 | } 70 | 71 | // Get AI client 72 | const ai = getAIClient(options.model); 73 | 74 | // Build system prompt 75 | const systemPrompt = buildSystemPrompt(prompt, context, options.hardhat, options.proxy); 76 | 77 | // Generate contract 78 | const result = await ai.generate(systemPrompt, { 79 | stream: options.stream !== false, 80 | }); 81 | 82 | // Parse the response 83 | const { code, securityNotes, testCode } = parseResponse(result.text); 84 | 85 | console.log('✅ Contract generated successfully.'); 86 | 87 | // Save the contract to a file if requested 88 | if (options.output && code) { 89 | // Create the output directory if it doesn't exist 90 | const outputDir = path.dirname(options.output); 91 | fs.mkdirSync(outputDir, { recursive: true }); 92 | 93 | fs.writeFileSync(options.output, code); 94 | console.log(`✅ Contract saved to ${options.output}`); 95 | 96 | // If Hardhat tests were requested and generated, save them too 97 | if (options.hardhat && testCode) { 98 | const testFilePath = path.join( 99 | path.dirname(options.output), 100 | `${path.basename(options.output, '.sol')}.test.js` 101 | ); 102 | fs.writeFileSync(testFilePath, testCode); 103 | console.log(`✅ Test file saved to ${testFilePath}`); 104 | } 105 | } 106 | 107 | return { 108 | code, 109 | securityNotes, 110 | testCode, 111 | }; 112 | } 113 | 114 | /** 115 | * Build system prompt for contract generation 116 | */ 117 | function buildSystemPrompt( 118 | prompt: string, 119 | context: string, 120 | generateTests: boolean = false, 121 | proxy?: 'transparent' | 'uups' 122 | ): string { 123 | let proxyGuideline = ''; 124 | if (proxy === 'transparent') { 125 | proxyGuideline = '\n9. Implement upgradeability using OpenZeppelin TransparentUpgradeableProxy pattern and organize the implementation, proxy, and deployment scripts in a clear folder structure (e.g., contracts/, proxy/, scripts/).'; 126 | } else if (proxy === 'uups') { 127 | proxyGuideline = '\n9. Implement upgradeability using OpenZeppelin UUPS pattern (UUPSUpgradeable) and organize the implementation and deployment scripts in a clear folder structure (e.g., contracts/, proxy/, scripts/).'; 128 | } 129 | 130 | return `You are an expert Solidity developer specializing in secure smart contract development. 131 | Generate a secure, well-documented smart contract based on the following requirements: 132 | 133 | ${prompt} 134 | 135 | ${context ? `Additional context:\n${context}\n` : ''} 136 | 137 | Follow these guidelines: 138 | 1. Use Solidity version 0.8.20 or higher 139 | 2. Follow security best practices 140 | 3. Use OpenZeppelin contracts for standard functionality 141 | 4. Include comprehensive NatSpec documentation 142 | 5. Include appropriate access control measures 143 | 6. Implement proper input validation 144 | 7. Use events for state changes 145 | 8. Protect against common vulnerabilities 146 | ${generateTests ? '9. Include Hardhat test cases to verify the contract functionality' : ''}${proxyGuideline} 147 | 148 | Respond with: 149 | 1. The complete Solidity contract code 150 | 2. Security considerations and notes 151 | ${generateTests ? '3. Hardhat test code' : ''}`; 152 | } 153 | 154 | /** 155 | * Parse the AI response to extract code, security notes, and test code 156 | */ 157 | function parseResponse(response: string): { 158 | code: string; 159 | securityNotes: string; 160 | testCode?: string 161 | } { 162 | // This is a simplified implementation 163 | // A real implementation would use regex or parsing to extract sections 164 | const sections = response.split('## '); 165 | 166 | let code = ''; 167 | let securityNotes = ''; 168 | let testCode = undefined; 169 | 170 | for (const section of sections) { 171 | if (section.startsWith('Solidity Contract') || section.startsWith('Contract Code')) { 172 | code = extractCode(section); 173 | } else if (section.startsWith('Security Considerations')) { 174 | securityNotes = section.replace('Security Considerations', '').trim(); 175 | } else if (section.startsWith('Test Code') || section.startsWith('Hardhat Tests')) { 176 | testCode = extractCode(section); 177 | } 178 | } 179 | 180 | return { 181 | code, 182 | securityNotes, 183 | testCode, 184 | }; 185 | } 186 | 187 | /** 188 | * Extract code from a section 189 | */ 190 | function extractCode(section: string): string { 191 | const codeMatch = section.match(/```solidity\n([\s\S]*?)\n```/) || 192 | section.match(/```javascript\n([\s\S]*?)\n```/) || 193 | section.match(/```\n([\s\S]*?)\n```/); 194 | return codeMatch ? codeMatch[1].trim() : ''; 195 | } 196 | 197 | /** 198 | * Get context from files 199 | */ 200 | async function getFileContext(filesPattern: string): Promise { 201 | // Implementation to read files matching the pattern 202 | const files = await loadFiles(filesPattern); 203 | return files.map(f => `${f.name}:\n"""\n${f.content}\n"""`).join('\n\n'); 204 | } 205 | 206 | /** 207 | * Parse HTML to text 208 | */ 209 | async function parseHtml(html: string): Promise { 210 | const parser = new DOMParser(); 211 | const doc = parser.parseFromString(html, 'text/html'); 212 | return doc.body.textContent || ''; 213 | } 214 | /** 215 | * Get context from URLs 216 | */ 217 | async function getUrlContext(url: string): Promise { 218 | // Implementation to fetch content from URLs 219 | const response = await fetch(url); 220 | const content = await response.text(); 221 | // parse the html to text 222 | const text = await parseHtml(content); 223 | return text; 224 | } 225 | 226 | /** 227 | * Get context from web search 228 | */ 229 | async function getSearchContext(query: string): Promise { 230 | const results = await getSearchResults(`solidity ${query} best practices`); 231 | return `Search Results:\n${results}`; 232 | } 233 | 234 | /** 235 | * Get context from vector database 236 | */ 237 | async function getVectorDBContext(collection: string, query: string): Promise { 238 | const vectorDB = new VectorDB(); 239 | const docs = await vectorDB.similaritySearch(collection, query, 5); 240 | return `VectorDB Results:\n${docs.map(d => d.pageContent || '').join('\n\n')}`; 241 | } -------------------------------------------------------------------------------- /src/services/contract/metamask-errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MetaMask Error Handling 3 | * 4 | * This module provides utilities for handling MetaMask error codes 5 | * based on the EIP-1193 and JSON RPC 2.0 standards. 6 | * Reference: https://medium.com/@social_42205/recognising-and-fixing-problem-codes-in-metamask-ae851060a05c 7 | */ 8 | 9 | /** 10 | * MetaMask error code descriptions 11 | */ 12 | export const METAMASK_ERROR_CODES: Record = { 13 | // EIP-1193 errors 14 | '4001': { 15 | standard: 'EIP-1193', 16 | message: 'User rejected the request.', 17 | solution: 'The user canceled the request. Try providing more information about why the action is needed.' 18 | }, 19 | '4100': { 20 | standard: 'EIP-1193', 21 | message: 'The requested account and/or method has not been authorized by the user.', 22 | solution: 'Request access to the user\'s accounts via wallet_requestPermissions first.' 23 | }, 24 | '4200': { 25 | standard: 'EIP-1193', 26 | message: 'The requested method is not supported by this Ethereum provider.', 27 | solution: 'Check for typos in the method name or if the method exists in the current provider.' 28 | }, 29 | '4900': { 30 | standard: 'EIP-1193', 31 | message: 'The provider is disconnected from all chains.', 32 | solution: 'The wallet is disconnected. Ask the user to check their internet connection and reload the page.' 33 | }, 34 | '4901': { 35 | standard: 'EIP-1193', 36 | message: 'The provider is disconnected from the specified chain.', 37 | solution: 'User needs to connect to the correct chain. Suggest switching networks.' 38 | }, 39 | 40 | // JSON-RPC 2.0 errors 41 | '-32700': { 42 | standard: 'JSON RPC 2.0', 43 | message: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', 44 | solution: 'Verify that your request object is valid JSON.' 45 | }, 46 | '-32600': { 47 | standard: 'JSON RPC 2.0', 48 | message: 'The JSON sent is not a valid Request object.', 49 | solution: 'Check that your request object follows the JSON-RPC 2.0 spec.' 50 | }, 51 | '-32601': { 52 | standard: 'JSON RPC 2.0', 53 | message: 'The method does not exist / is not available.', 54 | solution: 'Verify the method name is correct and supported by the current provider.' 55 | }, 56 | '-32602': { 57 | standard: 'JSON RPC 2.0', 58 | message: 'Invalid method parameter(s).', 59 | solution: 'Check that the parameters you\'re passing match what the method expects.' 60 | }, 61 | '-32603': { 62 | standard: 'JSON RPC 2.0', 63 | message: 'Internal JSON-RPC error.', 64 | solution: 'This is a general error. Check for: incorrect chain data, insufficient tokens for gas, or outdated MetaMask version.' 65 | }, 66 | 67 | // EIP-1474 errors 68 | '-32000': { 69 | standard: 'EIP-1474', 70 | message: 'Invalid input.', 71 | solution: 'Check contract address, ABI, or other parameters for accuracy.' 72 | }, 73 | '-32001': { 74 | standard: 'EIP-1474', 75 | message: 'Resource not found.', 76 | solution: 'The requested resource does not exist on the blockchain. Check for typos or non-existent entities.' 77 | }, 78 | '-32002': { 79 | standard: 'EIP-1474', 80 | message: 'Resource unavailable.', 81 | solution: 'The resource exists but is currently unavailable. Avoid rapid successive requests like multiple chain switching.' 82 | }, 83 | '-32003': { 84 | standard: 'EIP-1474', 85 | message: 'Transaction rejected.', 86 | solution: 'Check for: non-existent sender address, insufficient funds, locked account, or inability to sign the transaction.' 87 | }, 88 | '-32004': { 89 | standard: 'EIP-1474', 90 | message: 'Method not supported.', 91 | solution: 'The method is not supported by the current provider. Check for typos or if the method exists.' 92 | }, 93 | '-32005': { 94 | standard: 'EIP-1474', 95 | message: 'Request limit exceeded.', 96 | solution: 'You\'ve exceeded the rate limit. Implement exponential backoff or reduce request frequency.' 97 | } 98 | }; 99 | 100 | /** 101 | * Get error information for a MetaMask error code 102 | * 103 | * @param code Error code from MetaMask 104 | * @returns Error information object or undefined if not found 105 | */ 106 | export function getMetaMaskErrorInfo(code: string | number) { 107 | const codeStr = code.toString(); 108 | return METAMASK_ERROR_CODES[codeStr]; 109 | } 110 | 111 | /** 112 | * Handles a MetaMask error by providing useful information 113 | * 114 | * @param error The error object from MetaMask 115 | * @returns Formatted error message with solution 116 | */ 117 | export function handleMetaMaskError(error: any): string { 118 | let code: string | undefined; 119 | 120 | // Extract error code from various error formats 121 | if (typeof error === 'object') { 122 | if (error.code !== undefined) { 123 | code = error.code.toString(); 124 | } else if (error.error?.code !== undefined) { 125 | code = error.error.code.toString(); 126 | } else if (error.message && error.message.includes('code')) { 127 | // Try to extract code from error message 128 | const codeMatch = error.message.match(/code[: ]([0-9\-]+)/i); 129 | if (codeMatch && codeMatch[1]) { 130 | code = codeMatch[1]; 131 | } 132 | } 133 | } 134 | 135 | if (!code) { 136 | return `Unknown MetaMask error: ${error.message || JSON.stringify(error)}`; 137 | } 138 | 139 | const errorInfo = getMetaMaskErrorInfo(code); 140 | 141 | if (!errorInfo) { 142 | return `MetaMask error code ${code}: ${error.message || 'Unknown error'}`; 143 | } 144 | 145 | return `MetaMask error ${code} (${errorInfo.standard}): ${errorInfo.message}\n\nSolution: ${errorInfo.solution || 'No specific solution available.'}`; 146 | } 147 | 148 | /** 149 | * Augments a web3 function to handle MetaMask errors gracefully 150 | * 151 | * @param fn The function to wrap with error handling 152 | * @returns Wrapped function with error handling 153 | */ 154 | export function withMetaMaskErrorHandling Promise>(fn: T): T { 155 | return (async (...args: Parameters): Promise> => { 156 | try { 157 | return await fn(...args); 158 | } catch (error: any) { 159 | console.error(handleMetaMaskError(error)); 160 | throw error; 161 | } 162 | }) as T; 163 | } -------------------------------------------------------------------------------- /src/services/search/search.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Search Service 3 | * 4 | * This module provides functionality for web search to gather context for 5 | * smart contract generation. 6 | */ 7 | 8 | // In a real implementation, this would use an actual search API 9 | // like Bing, Google, or a specialized Web3/blockchain search service 10 | 11 | /** 12 | * Search result item 13 | */ 14 | export interface SearchResultItem { 15 | title: string; 16 | url: string; 17 | snippet: string; 18 | } 19 | 20 | /** 21 | * Mock search results for demo purposes 22 | */ 23 | const MOCK_SEARCH_RESULTS: Record = { 24 | 'solidity erc20': [ 25 | { 26 | title: 'ERC-20 Token Standard | ethereum.org', 27 | url: 'https://ethereum.org/en/developers/docs/standards/tokens/erc-20/', 28 | snippet: 'The ERC-20 introduces a standard for Fungible Tokens, in other words, they have a property that makes each Token be exactly the same in type and value as another Token.' 29 | }, 30 | { 31 | title: 'OpenZeppelin Contracts: ERC20', 32 | url: 'https://docs.openzeppelin.com/contracts/4.x/erc20', 33 | snippet: 'OpenZeppelin Contracts provides implementations of ERC20 with different levels of complexity and control.' 34 | } 35 | ], 36 | 'solidity security': [ 37 | { 38 | title: 'Smart Contract Security Best Practices | Consensys', 39 | url: 'https://consensys.github.io/smart-contract-best-practices/', 40 | snippet: 'This document provides a baseline knowledge of security considerations for intermediate Solidity programmers. It is maintained by ConsenSys Diligence.' 41 | }, 42 | { 43 | title: 'Smart Contract Weakness Classification (SWC) Registry', 44 | url: 'https://swcregistry.io/', 45 | snippet: 'The Smart Contract Weakness Classification Registry (SWC Registry) is an implementation of the weakness classification scheme proposed in EIP-1470.' 46 | } 47 | ] 48 | }; 49 | 50 | /** 51 | * Get search results for a query 52 | * 53 | * @param query The search query 54 | * @returns Formatted search results as text 55 | */ 56 | export async function getSearchResults(query: string): Promise { 57 | console.log(`Searching for: ${query}`); 58 | 59 | // Look for matching key in mock results, or use default 60 | const key = Object.keys(MOCK_SEARCH_RESULTS).find(k => 61 | query.toLowerCase().includes(k) 62 | ) || 'solidity security'; 63 | 64 | const results = MOCK_SEARCH_RESULTS[key]; 65 | 66 | // Format results as text 67 | return results.map(result => 68 | `Title: ${result.title}\nURL: ${result.url}\n${result.snippet}\n` 69 | ).join('\n'); 70 | } 71 | 72 | /** 73 | * For testing purposes only 74 | */ 75 | export function getMockSearchResult(query: string): Promise { 76 | return getSearchResults(query); 77 | } -------------------------------------------------------------------------------- /src/services/ui/chat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Chat Interface 3 | * 4 | * This module provides a simple chat interface for the CLI. 5 | */ 6 | import logUpdate from "log-update" 7 | import cliCursor from "cli-cursor" 8 | 9 | /** 10 | * Create a chat interface 11 | * 12 | * @returns Chat interface with methods to render messages 13 | */ 14 | export function createChat() { 15 | cliCursor.hide() 16 | 17 | function clear() { 18 | logUpdate("") 19 | } 20 | 21 | function render(message: string) { 22 | logUpdate(`${message}`) 23 | } 24 | 25 | function done() { 26 | logUpdate.done() 27 | cliCursor.show() 28 | } 29 | 30 | return { render, clear, done } 31 | } -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common Utilities 3 | * 4 | * This module provides common utility functions used throughout the application. 5 | */ 6 | import fs from "node:fs" 7 | import glob from "fast-glob" 8 | import { exec } from "node:child_process" 9 | 10 | /** 11 | * Check if a value is not empty 12 | * 13 | * @param value The value to check 14 | * @returns True if the value is not empty 15 | */ 16 | export function notEmpty( 17 | value: TValue | null | undefined | "" | false 18 | ): value is TValue { 19 | return ( 20 | value !== null && value !== undefined && value !== "" && value !== false 21 | ) 22 | } 23 | 24 | /** 25 | * Load files from globs 26 | * 27 | * @param files Files glob pattern 28 | * @returns Array of files with name and content 29 | */ 30 | export async function loadFiles( 31 | files: string | string[] 32 | ): Promise<{ name: string; content: string }[]> { 33 | if (!files || files.length === 0) return [] 34 | 35 | const filenames = await glob(files, { onlyFiles: true }) 36 | 37 | return await Promise.all( 38 | filenames.map(async (name) => { 39 | const content = await fs.promises.readFile(name, "utf8") 40 | return { name, content } 41 | }) 42 | ) 43 | } 44 | 45 | /** 46 | * Run a shell command 47 | * 48 | * @param command The command to run 49 | * @returns Command output 50 | */ 51 | export async function runCommand(command: string): Promise { 52 | return new Promise((resolve, reject) => { 53 | const cmd = exec(command) 54 | let output = "" 55 | cmd.stdout?.on("data", (data) => { 56 | output += data 57 | }) 58 | cmd.stderr?.on("data", (data) => { 59 | output += data 60 | }) 61 | cmd.on("close", () => { 62 | resolve(output) 63 | }) 64 | cmd.on("error", (error) => { 65 | reject(error) 66 | }) 67 | }) 68 | } -------------------------------------------------------------------------------- /src/utils/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error classes for the application 3 | */ 4 | import { bold, yellow, red } from 'colorette'; 5 | 6 | /** 7 | * Display a helpful message when a command is not found 8 | * @param commandName The command that was not found 9 | * @param availableCommands List of available commands 10 | */ 11 | export function showCommandNotFoundMessage(commandName: string, availableCommands: string[]): void { 12 | console.error(red(`Error: Unknown command '${commandName}'`)); 13 | 14 | const similarCommands = availableCommands 15 | .filter(cmd => cmd.startsWith(commandName[0]) || cmd.includes(commandName)) 16 | .slice(0, 3); 17 | 18 | if (similarCommands.length > 0) { 19 | console.log(yellow(`\nDid you mean one of these?`)); 20 | similarCommands.forEach(cmd => console.log(yellow(` ${cmd}`))); 21 | } 22 | 23 | console.log(`\nRun ${bold('web3cli --help')} to see all available commands.`); 24 | } 25 | 26 | /** 27 | * Base error class for CLI errors 28 | */ 29 | export class CliError extends Error { 30 | constructor(message: string) { 31 | super(message); 32 | this.name = 'CliError'; 33 | } 34 | } 35 | 36 | /** 37 | * Error for configuration issues 38 | */ 39 | export class ConfigError extends CliError { 40 | constructor(message: string) { 41 | super(`Configuration error: ${message}`); 42 | this.name = 'ConfigError'; 43 | } 44 | } 45 | 46 | /** 47 | * Error for API issues 48 | */ 49 | export class ApiError extends CliError { 50 | constructor(message: string) { 51 | super(`API error: ${message}`); 52 | this.name = 'ApiError'; 53 | } 54 | } 55 | 56 | /** 57 | * Error for file system issues 58 | */ 59 | export class FileSystemError extends CliError { 60 | constructor(message: string) { 61 | super(`File system error: ${message}`); 62 | this.name = 'FileSystemError'; 63 | } 64 | } 65 | 66 | /** 67 | * Error for contract generation issues 68 | */ 69 | export class ContractGenerationError extends CliError { 70 | constructor(message: string) { 71 | super(`Contract generation error: ${message}`); 72 | this.name = 'ContractGenerationError'; 73 | } 74 | } 75 | 76 | /** 77 | * Error for validation issues 78 | */ 79 | export class ValidationError extends CliError { 80 | constructor(message: string) { 81 | super(`Validation error: ${message}`); 82 | this.name = 'ValidationError'; 83 | } 84 | } 85 | 86 | /** 87 | * Error for when a command is not found 88 | */ 89 | export class CommandNotFoundError extends CliError { 90 | constructor(commandName: string) { 91 | super(`Unknown command: ${commandName}`); 92 | this.name = 'CommandNotFoundError'; 93 | this.commandName = commandName; 94 | } 95 | 96 | commandName: string; 97 | } -------------------------------------------------------------------------------- /src/utils/fetch-url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Fetching Utilities 3 | * 4 | * This module provides utilities for fetching content from URLs. 5 | */ 6 | 7 | /** 8 | * Fetch content from URLs 9 | * 10 | * @param urls URLs to fetch 11 | * @returns Array of URL and content pairs 12 | */ 13 | export async function fetchUrl( 14 | urls: string | string[] 15 | ): Promise<{ url: string; content: string }[]> { 16 | if (!urls || (Array.isArray(urls) && urls.length === 0)) return [] 17 | 18 | const urlArray = Array.isArray(urls) ? urls : [urls] 19 | return await Promise.all( 20 | urlArray.map(async (url) => { 21 | const resp = await fetch(url) 22 | const content = await resp.text() 23 | return { url, content } 24 | }) 25 | ) 26 | } -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | // Logger utility for uniform clean logs 2 | const origLog = console.log.bind(console); 3 | const origError = console.error.bind(console); 4 | 5 | export function step(message: string) { 6 | origLog(`➤ ${message}`); 7 | } 8 | 9 | export function success(message: string) { 10 | origLog(`✅ ${message}`); 11 | } 12 | 13 | export function fail(message: string) { 14 | origError(`❌ ${message}`); 15 | } 16 | 17 | export function detail(message: string) { 18 | console.error(` • ${message}`); 19 | } -------------------------------------------------------------------------------- /src/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Markdown Utilities 3 | * 4 | * This module provides utilities for rendering markdown content in the terminal. 5 | */ 6 | import terminalLink from "terminal-link" 7 | import { marked } from "marked" 8 | import TerminalRenderer from "marked-terminal" 9 | 10 | /** 11 | * Configure marked to use terminal renderer 12 | */ 13 | marked.setOptions({ 14 | // @ts-ignore - Type mismatch but this is the correct usage per docs 15 | renderer: new TerminalRenderer() 16 | }) 17 | 18 | /** 19 | * Render markdown content in the terminal 20 | * 21 | * @param content Markdown content to render 22 | * @returns Rendered content 23 | */ 24 | export function renderMarkdown(content: string): string { 25 | try { 26 | // Cast to string since marked types include Promise but in this usage it's synchronous 27 | return marked(content) as string 28 | } catch (error) { 29 | // If rendering fails, return the original content 30 | console.error("Error rendering markdown:", error) 31 | return content 32 | } 33 | } 34 | 35 | /** 36 | * Strip markdown code blocks from text 37 | * 38 | * Removes ```solidity, ```javascript, ```js, etc. markers from code blocks, 39 | * returning just the code content. If no code block markers are found, 40 | * returns the original text. 41 | * 42 | * @param text Text that might contain markdown code blocks 43 | * @returns Clean code without markdown formatting 44 | */ 45 | export function stripMarkdownCodeBlocks(text: string): string { 46 | if (!text || typeof text !== 'string') { 47 | return ''; 48 | } 49 | 50 | // Try multiple patterns to handle various markdown code block formats 51 | 52 | // 1. Try to match a code block with language specifier that spans the entire content 53 | // This is the most common when AI generates just the code 54 | const fullBlockWithLang = /^\s*```(?:solidity|javascript|js|typescript|ts)?\s*\n([\s\S]*?)\n```\s*$/; 55 | const fullMatch = text.match(fullBlockWithLang); 56 | if (fullMatch && fullMatch[1]) { 57 | return fullMatch[1].trim(); 58 | } 59 | 60 | // 2. Look for any code block with a Solidity language specifier 61 | const solidityBlock = /```solidity\s*\n([\s\S]*?)\n```/g; 62 | let match; 63 | let largestBlock = ''; 64 | 65 | while ((match = solidityBlock.exec(text)) !== null) { 66 | // Keep the largest matching block 67 | if (match[1] && match[1].length > largestBlock.length) { 68 | largestBlock = match[1].trim(); 69 | } 70 | } 71 | 72 | if (largestBlock) { 73 | return largestBlock; 74 | } 75 | 76 | // 3. Look for any code block (with or without a language specifier) 77 | const anyCodeBlock = /```(?:\w*)?\s*\n([\s\S]*?)\n```/g; 78 | largestBlock = ''; 79 | 80 | while ((match = anyCodeBlock.exec(text)) !== null) { 81 | // For multiple code blocks, prefer ones that look like Solidity 82 | const isLikelySolidity = match[1] && ( 83 | match[1].includes('pragma solidity') || 84 | match[1].includes('contract ') || 85 | match[1].includes('SPDX-License-Identifier') 86 | ); 87 | 88 | // Keep the largest matching block that is likely Solidity 89 | if (match[1] && (isLikelySolidity || largestBlock === '') && match[1].length > largestBlock.length) { 90 | largestBlock = match[1].trim(); 91 | } 92 | } 93 | 94 | if (largestBlock) { 95 | return largestBlock; 96 | } 97 | 98 | // 4. If we still don't have a match, check if the text itself contains Solidity code 99 | // without markdown markers (sometimes AI just outputs the code) 100 | if (text.includes('pragma solidity') || text.includes('contract ') || text.includes('SPDX-License-Identifier')) { 101 | // If it looks like raw Solidity code, return the whole text 102 | return text.trim(); 103 | } 104 | 105 | // 5. If all else fails, return the original text 106 | return text.trim(); 107 | } -------------------------------------------------------------------------------- /src/utils/tty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TTY Utilities 3 | * 4 | * This module provides utilities for working with terminal input/output. 5 | */ 6 | import process from "node:process" 7 | import tty from "node:tty" 8 | import fs from "node:fs" 9 | 10 | /** 11 | * Standard input stream 12 | */ 13 | export const stdin = 14 | process.stdin.isTTY || process.platform === "win32" 15 | ? process.stdin 16 | : new tty.ReadStream(fs.openSync("/dev/tty", "r")) 17 | 18 | /** 19 | * Whether standard output is a TTY 20 | */ 21 | export const isOutputTTY = process.stdout.isTTY 22 | 23 | /** 24 | * Read input from stdin pipe 25 | * 26 | * @returns Piped input as string or undefined if no pipe 27 | */ 28 | export async function readPipeInput(): Promise { 29 | // Check if data is being piped in 30 | if (process.stdin.isTTY || process.platform === "win32" && !process.stdin.isRaw) { 31 | return undefined; 32 | } 33 | 34 | return new Promise((resolve) => { 35 | const chunks: Buffer[] = []; 36 | 37 | process.stdin.on("data", (chunk) => { 38 | chunks.push(Buffer.from(chunk)); 39 | }); 40 | 41 | process.stdin.on("end", () => { 42 | const content = Buffer.concat(chunks).toString("utf8").trim(); 43 | resolve(content.length ? content : undefined); 44 | }); 45 | 46 | // Set a timeout in case stdin doesn't end 47 | setTimeout(() => { 48 | if (chunks.length) { 49 | const content = Buffer.concat(chunks).toString("utf8").trim(); 50 | resolve(content); 51 | } else { 52 | resolve(undefined); 53 | } 54 | }, 100); 55 | }); 56 | } 57 | 58 | /** 59 | * Check if the current process is running in a TTY 60 | * 61 | * @returns True if running in a TTY 62 | */ 63 | export function isTTY(): boolean { 64 | return Boolean(process.stdout.isTTY); 65 | } 66 | 67 | /** 68 | * Get the width of the terminal 69 | * 70 | * @returns Terminal width or default value 71 | */ 72 | export function getTerminalWidth(): number { 73 | return process.stdout.columns || 80; 74 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "nodenext" /* Specify what module code is generated. */, 29 | "rootDir": "src" /* Specify the root folder within your source files. */, 30 | "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "dist" /* Specify an output folder for all emitted files. */, 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | "noUnusedLocals": true, 94 | "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 95 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 96 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 97 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 98 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 99 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 100 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 101 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 102 | 103 | /* Completeness */ 104 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 105 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | import fs from "fs" 3 | 4 | const pkg = JSON.parse(fs.readFileSync("./package.json", "utf-8")) 5 | 6 | export default defineConfig({ 7 | entry: ["./src/cli.ts"], 8 | format: "esm", 9 | define: { 10 | PKG_VERSION: JSON.stringify(pkg.version), 11 | PKG_NAME: JSON.stringify(pkg.name), 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare const PKG_NAME: string 2 | declare const PKG_VERSION: string 3 | -------------------------------------------------------------------------------- /web3ailogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shivatmax/web3cli/8fefe87a31a1229f62c723fa0d57312592b372d9/web3ailogo.png -------------------------------------------------------------------------------- /web3cli.example.toml: -------------------------------------------------------------------------------- 1 | #:schema ./schema.json 2 | 3 | # Default model to use for AI completions 4 | # default_model = "gpt-4o-mini" 5 | 6 | # === OpenAI Configuration === 7 | # OpenAI API key - replace with your actual key 8 | # openai_api_key = "your-openai-api-key" 9 | # Optional: Custom OpenAI API URL (if using a proxy or compatible API) 10 | # openai_api_url = "https://custom-openai-endpoint.com/v1" 11 | 12 | # === Gemini Configuration === 13 | 14 | # gemini_api_key = "your-gemini-api-key" 15 | # gemini_api_url = "https://generativelanguage.googleapis.com" 16 | 17 | # === Anthropic Configuration === 18 | # Anthropic API key for Claude models 19 | # anthropic_api_key = "your-anthropic-api-key" 20 | 21 | # === Groq Configuration === 22 | # Groq API key for faster inference 23 | # groq_api_key = "your-groq-api-key" 24 | # groq_api_url = "https://api.groq.com/openai/v1" 25 | 26 | # === Mistral Configuration === 27 | # Mistral API key 28 | # mistral_api_key = "your-mistral-api-key" 29 | # mistral_api_url = "https://api.mistral.ai/v1" 30 | 31 | # === Ollama Configuration === 32 | # Ollama host for local models (default: http://localhost:11434) 33 | # ollama_host = "http://localhost:11434" 34 | 35 | # === Blockchain Tools === 36 | # Etherscan API key for fetching contract ABIs 37 | # etherscan_api_key = "your-etherscan-api-key" 38 | 39 | # === Custom Commands === 40 | # Optional: Configure custom commands 41 | [commands] 42 | # [commands.explain] 43 | # command = "explain" 44 | # prompt = "Explain the following smart contract code in plain English, highlighting key functions, permissions, and security patterns:" 45 | # require_stdin = true 46 | 47 | # [commands.generate] 48 | # command = "generate" 49 | # prompt = "Generate a secure Solidity smart contract based on these requirements:" 50 | # require_stdin = true 51 | --------------------------------------------------------------------------------