├── .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 | [](https://badge.fury.io/js/%40web3ai%2Fcli)
5 | [](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 |
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 |
--------------------------------------------------------------------------------