UNPKG

@web3ai/cli

Version:

Your AI-powered command-line companion for seamless Web3 development. Ask questions, get code suggestions, and accelerate your blockchain projects.

1,345 lines (1,309 loc) 111 kB
#!/usr/bin/env node import { VectorDB, configDirPath, loadConfig } from "./chunk-6TQAECFG.js"; // src/cli.ts import process4 from "node:process"; import { cac } from "cac"; import { bold as bold2, red as red2, yellow as yellow2, blue } from "colorette"; // src/services/ai/models.ts var MODEL_PREFIXES = { ANTHROPIC: "claude-", OPENAI: "gpt-", OPENAI_O: "openai-o", GEMINI: "gemini-", GROQ: "groq-", MISTRAL: "mistral-", COPILOT: "copilot-", OLLAMA: "ollama-" }; var AVAILABLE_MODELS = [ // OpenAI models { id: "gpt-4o-mini", realId: "gpt-4o-mini" }, { id: "gpt-4o", realId: "gpt-4o" }, { id: "gpt-4.1", realId: "gpt-4.1" }, { id: "gpt-4.1-mini", realId: "gpt-4.1-mini" }, { id: "gpt-4.1-nano", realId: "gpt-4.1-nano" }, { id: "gpt-3.5-turbo", realId: "gpt-3.5-turbo" }, // OpenAI "o" models { id: "openai-o1", realId: "o1" }, { id: "openai-o1-mini", realId: "o1-mini" }, { id: "openai-o1-preview", realId: "o1-preview" }, { id: "openai-o3-mini", realId: "o3-mini" }, { id: "openai-o3", realId: "o3" }, { id: "openai-o4-mini", realId: "o4-mini" }, // Claude models { id: "claude-3-7-sonnet", realId: "claude-3-7-sonnet-20240307" }, { id: "claude-3-5-sonnet", realId: "claude-3-5-sonnet-20240620" }, { id: "claude-3-5-haiku", realId: "claude-3-5-haiku-20240307" }, { id: "claude-3-opus", realId: "claude-3-opus-20240229" }, // Gemini models { id: "gemini-2.5-flash", realId: "gemini-2.5-flash-preview-04-17" }, { id: "gemini-2.5-pro", realId: "gemini-2.5-pro-preview-05-06" }, { id: "gemini-2.0-flash", realId: "gemini-2.0-flash" }, { id: "gemini-2.0-flash-lite", realId: "gemini-2.0-flash-lite" }, { id: "gemini-1.5-flash", realId: "gemini-1.5-flash-latest" }, { id: "gemini-1.5-pro", realId: "gemini-1.5-pro-latest" }, // Groq models { id: "groq-llama-3.3-70b", realId: "llama-3.3-70b-versatile" }, { id: "groq-llama-3.1-8b", realId: "llama-3.1-8b-instant" }, { id: "groq-mixtral-8x7b", realId: "mixtral-8x7b-32768" }, // Mistral models { id: "mistral-large", realId: "mistral-large-latest" }, { id: "mistral-medium", realId: "mistral-medium-latest" }, { id: "mistral-small", realId: "mistral-small-latest" }, // GitHub Copilot models { id: "copilot-gpt-4o", realId: "gpt-4o" }, { id: "copilot-o1-mini", realId: "o1-mini" }, { id: "copilot-o1-preview", realId: "o1-preview" }, { id: "copilot-claude-3.5-sonnet", realId: "claude-3.5-sonnet" } ]; async function getAllModels(includeAll = false, includeOllama = false) { const models = [...AVAILABLE_MODELS]; if (includeAll) { return models; } if (includeOllama) { try { const host = process.env.OLLAMA_HOST || "http://localhost:11434"; const response = await fetch(`${host}/api/tags`); if (response.ok) { const data = await response.json(); if (data.models && Array.isArray(data.models)) { data.models.forEach((model) => { if (model.name) { models.push({ id: `ollama-${model.name}`, realId: model.name }); } }); console.log(`Found ${data.models.length} local Ollama models`); } } else { console.warn("Failed to connect to Ollama server - is it running?"); models.push({ id: "ollama-llama3", realId: "llama3" }); } } catch (error) { console.warn("Failed to fetch Ollama models:", error); models.push({ id: "ollama-llama3", realId: "llama3" }); } } return models; } function getModelProvider(modelId) { if (modelId.startsWith(MODEL_PREFIXES.ANTHROPIC)) { return "anthropic"; } if (modelId.startsWith(MODEL_PREFIXES.GEMINI)) { return "gemini"; } if (modelId.startsWith(MODEL_PREFIXES.GROQ)) { return "groq"; } if (modelId.startsWith(MODEL_PREFIXES.MISTRAL)) { return "mistral"; } if (modelId.startsWith(MODEL_PREFIXES.COPILOT)) { return "copilot"; } if (modelId.startsWith(MODEL_PREFIXES.OLLAMA)) { return "ollama"; } return "openai"; } function getRealModelId(modelId) { const model = AVAILABLE_MODELS.find((m) => m.id === modelId); return model?.realId || modelId; } // src/cli.ts import updateNotifier from "update-notifier"; // src/services/ai/ask.ts import process3 from "node:process"; // src/utils/common.ts import fs from "node:fs"; import glob from "fast-glob"; function notEmpty(value) { return value !== null && value !== void 0 && value !== "" && value !== false; } async function loadFiles(files) { if (!files || files.length === 0) return []; const filenames = await glob(files, { onlyFiles: true }); return await Promise.all( filenames.map(async (name) => { const content = await fs.promises.readFile(name, "utf8"); return { name, content }; }) ); } // src/services/ai/ask.ts import cliPrompts from "prompts"; // src/utils/tty.ts import process2 from "node:process"; import tty from "node:tty"; import fs2 from "node:fs"; var stdin = process2.stdin.isTTY || process2.platform === "win32" ? process2.stdin : new tty.ReadStream(fs2.openSync("/dev/tty", "r")); var isOutputTTY = process2.stdout.isTTY; async function readPipeInput() { if (process2.stdin.isTTY || process2.platform === "win32" && !process2.stdin.isRaw) { return void 0; } return new Promise((resolve) => { const chunks = []; process2.stdin.on("data", (chunk) => { chunks.push(Buffer.from(chunk)); }); process2.stdin.on("end", () => { const content = Buffer.concat(chunks).toString("utf8").trim(); resolve(content.length ? content : void 0); }); setTimeout(() => { if (chunks.length) { const content = Buffer.concat(chunks).toString("utf8").trim(); resolve(content); } else { resolve(void 0); } }, 100); }); } // src/utils/error.ts import { bold, yellow, red } from "colorette"; function showCommandNotFoundMessage(commandName, availableCommands) { console.error(red(`Error: Unknown command '${commandName}'`)); const similarCommands = availableCommands.filter((cmd) => cmd.startsWith(commandName[0]) || cmd.includes(commandName)).slice(0, 3); if (similarCommands.length > 0) { console.log(yellow(` Did you mean one of these?`)); similarCommands.forEach((cmd) => console.log(yellow(` ${cmd}`))); } console.log(` Run ${bold("web3cli --help")} to see all available commands.`); } var CliError = class extends Error { constructor(message) { super(message); this.name = "CliError"; } }; var ValidationError = class extends CliError { constructor(message) { super(`Validation error: ${message}`); this.name = "ValidationError"; } }; var CommandNotFoundError = class extends CliError { constructor(commandName) { super(`Unknown command: ${commandName}`); this.name = "CommandNotFoundError"; this.commandName = commandName; } commandName; }; // src/services/ai/ai-sdk.ts import OpenAI from "openai"; import path from "node:path"; import { GoogleGenerativeAI } from "@google/generative-ai"; import { Anthropic } from "@anthropic-ai/sdk"; async function getSDKModel(modelId, config) { const provider = getModelProvider(modelId); const realModelId = getRealModelId(modelId); try { switch (provider) { case "anthropic": return getAnthropicClient(config); case "gemini": return getGeminiClient(config, realModelId); case "groq": return getGroqClient(config); case "mistral": return getMistralClient(config); case "copilot": return getCopilotClient(config); case "ollama": return getOllamaClient(config); case "openai": default: return getOpenAIClient(config); } } catch (error) { const e = error; if (e.message.includes("API key not found")) { const localPath = path.join(process.cwd(), "web3cli.toml"); const globalPath = path.join(configDirPath, "web3cli.toml"); throw new Error( `${provider.charAt(0).toUpperCase() + provider.slice(1)} API key not configured. Please set the ${provider.toUpperCase()}_API_KEY environment variable, or add ${provider.toLowerCase()}_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` ); } throw error; } } function getOpenAIClient(config) { if (!config.openai_api_key) { const localPath = path.join(process.cwd(), "web3cli.toml"); const globalPath = path.join(configDirPath, "web3cli.toml"); throw new Error( `OpenAI API key not found. Please set the OPENAI_API_KEY environment variable, or add openai_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` ); } const baseURL = config.openai_api_url || process.env.OPENAI_API_URL; const openaiOptions = { apiKey: config.openai_api_key }; if (baseURL) { openaiOptions.baseURL = baseURL; } return new OpenAI(openaiOptions); } function getAnthropicClient(config) { if (!config.anthropic_api_key) { const localPath = path.join(process.cwd(), "web3cli.toml"); const globalPath = path.join(configDirPath, "web3cli.toml"); throw new Error( `Anthropic API key not found. Please set the ANTHROPIC_API_KEY environment variable, or add anthropic_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` ); } const anthropic = new Anthropic({ apiKey: config.anthropic_api_key }); return { chat: { completions: { create: async ({ messages, stream = false, ...options }) => { try { let systemPrompt = ""; const anthropicMessages = messages.map((msg) => { if (msg.role === "system") { systemPrompt = msg.content; return null; } return { role: msg.role === "assistant" ? "assistant" : "user", content: msg.content }; }).filter(Boolean); if (stream) { const streamingResponse = await anthropic.beta.messages.create({ model: options.model || "claude-3-5-sonnet-20240620", messages: anthropicMessages, system: systemPrompt, stream: true, max_tokens: options.max_tokens || 4096, temperature: options.temperature || 0 }); return { [Symbol.asyncIterator]: async function* () { for await (const chunk of streamingResponse) { if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") { yield { choices: [{ delta: { content: chunk.delta.text } }] }; } } } }; } else { const response = await anthropic.beta.messages.create({ model: options.model || "claude-3-5-sonnet-20240620", messages: anthropicMessages, system: systemPrompt, max_tokens: options.max_tokens || 4096, temperature: options.temperature || 0 }); return { choices: [{ message: { content: response.content[0].type === "text" ? response.content[0].text : response.content[0] } }] }; } } catch (error) { console.error("Anthropic API error:", error); throw error; } } } } }; } function getGeminiClient(config, modelName) { if (!config.gemini_api_key) { const localPath = path.join(process.cwd(), "web3cli.toml"); const globalPath = path.join(configDirPath, "web3cli.toml"); throw new Error( `Gemini API key not found. Please set the GEMINI_API_KEY environment variable, or add gemini_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` ); } const genAI = new GoogleGenerativeAI(config.gemini_api_key); const model = genAI.getGenerativeModel({ model: modelName }); return { chat: { completions: { create: async ({ messages, stream = false }) => { const prompt = messages.map((msg) => { if (msg.role === "system") { return { role: "user", parts: [{ text: msg.content }] }; } return { role: msg.role === "assistant" ? "model" : "user", parts: [{ text: msg.content }] }; }); try { if (stream) { const streamingResponse = await model.generateContentStream({ contents: prompt }); return { [Symbol.asyncIterator]: async function* () { for await (const chunk of streamingResponse.stream) { const text = chunk.text(); yield { choices: [{ delta: { content: text } }] }; } } }; } else { const response = await model.generateContent({ contents: prompt }); return { choices: [{ message: { content: response.response.text() } }] }; } } catch (error) { console.error("Gemini API error:", error); throw error; } } } } }; } function getGroqClient(config) { if (!config.groq_api_key) { const localPath = path.join(process.cwd(), "web3cli.toml"); const globalPath = path.join(configDirPath, "web3cli.toml"); throw new Error( `Groq API key not found. Please set the GROQ_API_KEY environment variable, or add groq_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` ); } const baseURL = config.groq_api_url || "https://api.groq.com/openai/v1"; return new OpenAI({ apiKey: config.groq_api_key, baseURL }); } function getMistralClient(config) { if (!config.mistral_api_key) { const localPath = path.join(process.cwd(), "web3cli.toml"); const globalPath = path.join(configDirPath, "web3cli.toml"); throw new Error( `Mistral API key not found. Please set the MISTRAL_API_KEY environment variable, or add mistral_api_key to your web3cli.toml configuration file (${localPath} or ${globalPath}).` ); } const baseURL = config.mistral_api_url || "https://api.mistral.ai/v1"; return new OpenAI({ apiKey: config.mistral_api_key, baseURL }); } function getCopilotClient(config) { console.warn("Using OpenAI as a fallback for Copilot models - proper Copilot API access not implemented"); return getOpenAIClient(config); } function getOllamaClient(config) { const host = config.ollama_host || "http://localhost:11434"; return { chat: { completions: { create: async ({ messages, stream = false, model: modelName, ...options }) => { try { const ollama_messages = messages.map((msg) => { return { role: msg.role === "system" ? "user" : msg.role, content: msg.content }; }); const modelToUse = modelName || "llama3"; if (stream) { const response = await fetch(`${host}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: modelToUse, messages: ollama_messages, stream: true, options: { temperature: options.temperature || 0 } }) }); if (!response.ok) { throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); } if (!response.body) { throw new Error("Ollama response body is null"); } const reader = response.body.getReader(); const decoder = new TextDecoder(); return { [Symbol.asyncIterator]: async function* () { while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split("\n").filter(Boolean); for (const line of lines) { try { const data = JSON.parse(line); if (data.message?.content) { yield { choices: [{ delta: { content: data.message.content } }] }; } } catch (e) { console.warn("Failed to parse Ollama chunk:", line); } } } } }; } else { const response = await fetch(`${host}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: modelToUse, messages: ollama_messages, options: { temperature: options.temperature || 0 } }) }); if (!response.ok) { throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return { choices: [{ message: { content: data.message?.content || "No content returned from Ollama" } }] }; } } catch (error) { console.error("Ollama API error:", error); throw error; } } } } }; } // src/utils/fetch-url.ts async function fetchUrl(urls) { if (!urls || Array.isArray(urls) && urls.length === 0) return []; const urlArray = Array.isArray(urls) ? urls : [urls]; return await Promise.all( urlArray.map(async (url) => { const resp = await fetch(url); const content = await resp.text(); return { url, content }; }) ); } // src/services/ai/ask.ts import logUpdate from "log-update"; // src/utils/markdown.ts import { marked } from "marked"; import TerminalRenderer from "marked-terminal"; marked.setOptions({ // @ts-ignore - Type mismatch but this is the correct usage per docs renderer: new TerminalRenderer() }); function renderMarkdown(content) { try { return marked(content); } catch (error) { console.error("Error rendering markdown:", error); return content; } } function stripMarkdownCodeBlocks(text) { if (!text || typeof text !== "string") { return ""; } const fullBlockWithLang = /^\s*```(?:solidity|javascript|js|typescript|ts)?\s*\n([\s\S]*?)\n```\s*$/; const fullMatch = text.match(fullBlockWithLang); if (fullMatch && fullMatch[1]) { return fullMatch[1].trim(); } const solidityBlock = /```solidity\s*\n([\s\S]*?)\n```/g; let match; let largestBlock = ""; while ((match = solidityBlock.exec(text)) !== null) { if (match[1] && match[1].length > largestBlock.length) { largestBlock = match[1].trim(); } } if (largestBlock) { return largestBlock; } const anyCodeBlock = /```(?:\w*)?\s*\n([\s\S]*?)\n```/g; largestBlock = ""; while ((match = anyCodeBlock.exec(text)) !== null) { const isLikelySolidity = match[1] && (match[1].includes("pragma solidity") || match[1].includes("contract ") || match[1].includes("SPDX-License-Identifier")); if (match[1] && (isLikelySolidity || largestBlock === "") && match[1].length > largestBlock.length) { largestBlock = match[1].trim(); } } if (largestBlock) { return largestBlock; } if (text.includes("pragma solidity") || text.includes("contract ") || text.includes("SPDX-License-Identifier")) { return text.trim(); } return text.trim(); } // src/services/ai/rag-utils.ts loadConfig(); async function getRelevantContent(query, collectionName, k = 5) { const db = new VectorDB(); try { const results = await db.search(collectionName, query, k); if (results.length === 0) { return ""; } let context = `## Relevant information from ${collectionName} documentation: `; results.forEach((doc) => { const source = doc.metadata?.source || "Unknown source"; context += `### Source: ${source} ${doc.pageContent} --- `; }); return context; } catch (error) { console.error(`Error retrieving content from collection '${collectionName}':`, error); return ""; } } async function processVectorDBReadRequest(query, collectionName, k = 5) { try { const content = await getRelevantContent(query, collectionName, k); if (!content) { return `No relevant information found in collection '${collectionName}' for query: ${query}`; } return content; } catch (error) { return `Error retrieving information: ${error.message || error}`; } } // src/services/ai/ask.ts var debug = (...args) => { if (process3.env.DEBUG !== "shell-ask" && process3.env.DEBUG !== "*") return; console.log(...args); }; async function ask(prompt, options) { if (!prompt) { throw new CliError("please provide a prompt"); } const config = loadConfig(); let modelId = options.model === true ? "select" : options.model || config.default_model || "gpt-4o-mini"; const includeOllama = modelId === "select" || modelId.startsWith("ollama-"); const models = await getAllModels( modelId === "select" ? true : false, includeOllama ); if (modelId === "select") { if (process3.platform === "win32" && !process3.stdin.isTTY) { throw new CliError( "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`" ); } const result = await cliPrompts([ { stdin, type: "autocomplete", message: "Select a model", name: "modelId", async suggest(input, choices) { return choices.filter((choice) => { return choice.title.toLowerCase().includes(input); }); }, choices: models.filter( (item) => modelId === "select" || item.id.startsWith(`${modelId}-`) ).map((item) => { return { value: item.id, title: item.id }; }) } ]); if (typeof result.modelId !== "string" || !result.modelId) { throw new CliError("no model selected"); } modelId = result.modelId; } debug(`Selected modelID: ${modelId}`); const matchedModel = models.find( (m) => m.id === modelId || m.realId === modelId ); if (!matchedModel) { const modelPrefix = modelId.split("-")[0]; const similarModels = models.filter((m) => m.id.startsWith(`${modelPrefix}-`)).map((m) => m.id); let errorMessage = `Model not found: ${modelId} `; if (similarModels.length > 0) { errorMessage += `Did you mean one of these models? ${similarModels.join("\n")} `; } errorMessage += `Available models: ${models.map((m) => m.id).join(", ")}`; throw new CliError(errorMessage); } const realModelId = matchedModel.realId || modelId; const openai = await getSDKModel(modelId, config); debug("model", realModelId); const files = await loadFiles(options.files || []); const remoteContents = await fetchUrl(options.url || []); let docsContext = []; if (options.readDocs) { try { const docsContent = await processVectorDBReadRequest(prompt, options.readDocs, 8); if (docsContent) { docsContext = [ `docs:${options.readDocs}:`, `""" ${docsContent} """` ]; } } catch (e) { console.warn("Warning: Failed to retrieve docs from vector DB", e); } } const context = [ `platform: ${process3.platform} shell: ${process3.env.SHELL || "unknown"}`, options.pipeInput && [`stdin:`, "```", options.pipeInput, "```"].join("\n"), files.length > 0 && "files:", ...files.map((file) => `${file.name}: """ ${file.content} """`), remoteContents.length > 0 && "remote contents:", ...remoteContents.map( (content) => `${content.url}: """ ${content.content} """` ), ...docsContext ].filter(notEmpty).join("\n"); let searchResult; if (options.search) { console.log("Web search is not currently available"); } 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.`; const userMessage = [ searchResult && `SEARCH RESULTS: ${searchResult}`, context && `CONTEXT: ${context}`, `QUESTION: ${prompt}` ].filter(Boolean).join("\n\n"); try { let content = ""; let messages = [ { role: "system", content: systemMessage }, { role: "user", content: userMessage } ]; if (options.readDocs) { console.log(`Using RAG with collection: ${options.readDocs}`); try { const docsContent = await processVectorDBReadRequest(prompt, options.readDocs, 5); if (docsContent) { messages[1] = { role: "user", content: `${userMessage} Here's some relevant information to help you answer: ${docsContent}` }; console.log("\u2705 Context from vector database added to prompt"); } } catch (error) { console.warn("Warning: Failed to augment messages with RAG", error); } } const provider = getModelProvider(modelId); console.log(`Using ${provider.toUpperCase()} provider with model: ${realModelId}`); if (options.stream !== false) { try { const stream = await openai.chat.completions.create({ model: realModelId, messages, stream: true }); const streamWithIterator = stream; for await (const chunk of streamWithIterator) { const content_chunk = chunk.choices?.[0]?.delta?.content || ""; content += content_chunk; logUpdate(renderMarkdown(content)); } logUpdate.done(); } catch (error) { logUpdate.clear(); if (error.message && error.message.includes("does not exist")) { console.error(`Error: Model '${realModelId}' not found or not available with the ${provider} provider.`); console.error(` Make sure you've configured the API key for ${provider} and are using a valid model.`); console.error(`To see all available models, run: web3cli list`); } else { console.error("Error during streaming request:", error.message || error); } } } else { try { const completion = await openai.chat.completions.create({ model: realModelId, messages }); content = completion.choices?.[0]?.message?.content || "No response generated"; console.log(renderMarkdown(content)); } catch (error) { if (error.message && error.message.includes("does not exist")) { console.error(`Error: Model '${realModelId}' not found or not available with the ${provider} provider.`); console.error(` Make sure you've configured the API key for ${provider} and are using a valid model.`); console.error(`To see all available models, run: web3cli list`); } else { console.error("Error during request:", error.message || error); } } } } catch (error) { console.error("Error during request:", error); } } // src/cli.ts import { APICallError } from "ai"; // src/services/contract/generate-contract.ts import fs3 from "node:fs"; import path2 from "node:path"; import logUpdate2 from "log-update"; async function generateContract(prompt, options = {}) { if (!prompt) { throw new CliError("Please provide a prompt describing the smart contract"); } console.log("Generating smart contract..."); const config = loadConfig(); const modelId = options.model || config.default_model || "gpt-4o-mini"; const openai = await getSDKModel(modelId, config); const files = await loadFiles(options.files || []); const remoteContents = await fetchUrl(options.url || []); let docsContext = []; if (options.readDocs) { try { const vdb = new VectorDB(); const docs = await vdb.similaritySearch(options.readDocs, prompt, 8); if (docs.length > 0) { docsContext = [ `docs:${options.readDocs}:`, ...docs.map((d) => `""" ${d.text || d.pageContent || ""} """`) ]; } } catch (e) { console.warn("Warning: Could not retrieve docs from vector DB:", e); } } const context = [ `platform: ${process.platform} solidity: ^0.8.20`, options.pipeInput && [`stdin:`, "```", options.pipeInput, "```"].join("\n"), files.length > 0 && "files:", ...files.map((file) => `${file.name}: """ ${file.content} """`), remoteContents.length > 0 && "remote contents:", ...remoteContents.map( (content) => `${content.url}: """ ${content.content} """` ), ...docsContext ].filter(notEmpty).join("\n"); let proxyGuideline = ""; if (options.proxy === "transparent") { 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/."; } else if (options.proxy === "uups") { 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/."; } const messages = [ { role: "system", content: `You are an expert Solidity developer who specializes in creating secure, efficient, and well-documented smart contracts. Output only valid Solidity code without additional explanations. The contract should: - Use the most recent Solidity version (^0.8.20) - Be secure, following all best practices - Use appropriate OpenZeppelin contracts when relevant - Include comprehensive NatSpec documentation - Be gas-efficient - Include appropriate events, modifiers, and access control ${options.hardhat ? "After the contract, include a Hardhat test file that thoroughly tests the contract functionality." : ""} ${proxyGuideline} ` }, { role: "user", content: [ context && `CONTEXT: ${context}`, `TASK: Generate a Solidity smart contract for the following requirements:`, prompt ].filter(Boolean).join("\n\n") } ]; try { let content = ""; if (options.stream !== false) { const stream = await openai.chat.completions.create({ model: modelId, messages, stream: true }); for await (const chunk of stream) { const content_chunk = chunk.choices[0]?.delta?.content || ""; content += content_chunk; logUpdate2(renderMarkdown(content)); } logUpdate2.done(); } else { const completion = await openai.chat.completions.create({ model: modelId, messages }); content = completion.choices?.[0]?.message?.content || ""; console.log(renderMarkdown(content)); } if (options.output) { const cleanContent = stripMarkdownCodeBlocks(content); const outputDir = path2.dirname(options.output); fs3.mkdirSync(outputDir, { recursive: true }); fs3.writeFileSync(options.output, cleanContent); console.log(` \u2705 Contract saved to ${options.output}`); if (options.hardhat && content.includes("// Test file")) { const testParts = content.split(/\/\/ Test file/); if (testParts.length >= 2) { const testContent = testParts[1].trim(); const cleanTestContent = stripMarkdownCodeBlocks(testContent); const testPath = options.output.replace(/\.sol$/, ".test.js"); fs3.writeFileSync(testPath, cleanTestContent); console.log(`\u2705 Test file saved to ${testPath}`); } } } return { code: content }; } catch (error) { console.error("Error generating contract:", error); throw error; } } // src/services/contract/agent-mode.ts import fs4 from "node:fs"; // src/services/search/search.ts var MOCK_SEARCH_RESULTS = { "solidity erc20": [ { title: "ERC-20 Token Standard | ethereum.org", url: "https://ethereum.org/en/developers/docs/standards/tokens/erc-20/", 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." }, { title: "OpenZeppelin Contracts: ERC20", url: "https://docs.openzeppelin.com/contracts/4.x/erc20", snippet: "OpenZeppelin Contracts provides implementations of ERC20 with different levels of complexity and control." } ], "solidity security": [ { title: "Smart Contract Security Best Practices | Consensys", url: "https://consensys.github.io/smart-contract-best-practices/", snippet: "This document provides a baseline knowledge of security considerations for intermediate Solidity programmers. It is maintained by ConsenSys Diligence." }, { title: "Smart Contract Weakness Classification (SWC) Registry", url: "https://swcregistry.io/", snippet: "The Smart Contract Weakness Classification Registry (SWC Registry) is an implementation of the weakness classification scheme proposed in EIP-1470." } ] }; async function getSearchResults(query) { console.log(`Searching for: ${query}`); const key = Object.keys(MOCK_SEARCH_RESULTS).find( (k) => query.toLowerCase().includes(k) ) || "solidity security"; const results = MOCK_SEARCH_RESULTS[key]; return results.map( (result) => `Title: ${result.title} URL: ${result.url} ${result.snippet} ` ).join("\n"); } // src/services/contract/agent-mode.ts import logUpdate3 from "log-update"; import path3 from "path"; import ora from "ora"; function logAgentActivity(agent, message, context) { const logMessage = `${agent.emoji} ${agent.name} Agent --> ${message}`; console.log(logMessage); context.agentLog.push(logMessage); } function ensureDirectoryExists(dirPath) { try { fs4.mkdirSync(dirPath, { recursive: true }); } catch (error) { if (error.code !== "EEXIST") { throw error; } } } function safeWriteFileSync(filePath, content) { try { const dir = path3.dirname(filePath); ensureDirectoryExists(dir); fs4.writeFileSync(filePath, content); return true; } catch (error) { console.error(`\u274C Error writing to ${filePath}:`, error); return false; } } async function runAgentMode(prompt, options = {}) { console.log("\u27A4 \u{1F680} Initializing Advanced Agentic System\u2026"); console.log("\u27A4 \u{1F4CA} Setting up specialized agents\u2026"); try { const config = loadConfig(); const modelId = options.model || config.default_model || "gpt-4o-mini"; const openai = await getSDKModel(modelId, config); const context = { prompt, options, openai, modelId, agentLog: [] }; const agents = [ // Vector Store Search Agent { name: "Vector Store Search", description: "Searches vector database for relevant documentation", emoji: "\u{1F4DA}", async execute(input, context2) { logAgentActivity(this, "Analyzing prompt to create optimal search queries", context2); if (!context2.options.readDocs) { logAgentActivity(this, "No vector database specified, skipping search", context2); return null; } logAgentActivity(this, `Searching vector database for information about: ${input}`, context2); try { const db = new VectorDB(); const keyTerms = await this.extractKeyTerms?.(input, context2) || []; context2.vectorSearchResults = []; logAgentActivity(this, `Identified key search terms: ${keyTerms.join(", ")}`, context2); for (const term of keyTerms) { logAgentActivity(this, `Searching for: ${term}`, context2); const docs = await db.similaritySearch(context2.options.readDocs, term, 3); if (docs.length > 0) { const results = docs.map((d) => d.pageContent || "").join("\n\n"); context2.vectorSearchResults.push(`Results for "${term}": ${results}`); logAgentActivity(this, `Found ${docs.length} relevant documents for "${term}"`, context2); } } return context2.vectorSearchResults.length > 0 ? context2.vectorSearchResults.join("\n\n---\n\n") : "No relevant documentation found."; } catch (e) { logAgentActivity(this, `Error searching vector database: ${e}`, context2); return "Error searching vector database."; } }, async extractKeyTerms(input, context2) { logAgentActivity(this, "Extracting key terms for search", context2); const messages = [ { role: "system", content: `You are an expert in creating search queries from user requirements. Extract 3-5 key technical terms or concepts from the input that would be most useful for searching technical documentation. Return ONLY a JSON array of strings without any explanation.` }, { role: "user", content: input } ]; const response = await context2.openai.chat.completions.create({ model: context2.modelId, messages, response_format: { type: "json_object" } }); try { const content = response.choices[0].message.content; const parsed = JSON.parse(content); return parsed.terms || []; } catch (e) { logAgentActivity(this, `Error parsing key terms: ${e}`, context2); const words = input.split(/\s+/).filter((w) => w.length > 4); return words.slice(0, 3); } } }, // Web Search Agent { name: "Web Search", description: "Searches the web for relevant information", emoji: "\u{1F50E}", async execute(input, context2) { if (!context2.options.search) { logAgentActivity(this, "Web search not requested, skipping", context2); return null; } logAgentActivity(this, "Analyzing prompt to create optimal web search queries", context2); const searchQueries = await this.generateSearchQueries?.(input, context2) || [`solidity ${input.slice(0, 50)}`]; context2.webSearchResults = []; logAgentActivity(this, `Generated search queries: ${searchQueries.join(", ")}`, context2); for (const query of searchQueries) { logAgentActivity(this, `Searching web for: ${query}`, context2); const results = await getSearchResults(query); context2.webSearchResults.push(`Results for "${query}": ${results}`); logAgentActivity(this, `Completed search for: ${query}`, context2); } return context2.webSearchResults.join("\n\n---\n\n"); }, async generateSearchQueries(input, context2) { const messages = [ { role: "system", content: `You are an expert in creating web search queries from user requirements. Generate 2-3 specific search queries related to Solidity and blockchain development that would help find relevant information for the given task. Return ONLY a JSON array of strings without any explanation.` }, { role: "user", content: input } ]; const response = await context2.openai.chat.completions.create({ model: context2.modelId, messages, response_format: { type: "json_object" } }); try { const content = response.choices[0].message.content; const parsed = JSON.parse(content); return parsed.queries || []; } catch (e) { logAgentActivity(this, `Error parsing search queries: ${e}`, context2); return [`solidity ${input.slice(0, 50)}...`]; } } }, // Code Writing Agent { name: "Code Writer", description: "Writes Solidity smart contract code", emoji: "\u270D\uFE0F", async execute(input, context2) { logAgentActivity(this, "Starting smart contract generation", context2); let contextInfo = ""; if (context2.webSearchResults && context2.webSearchResults.length > 0) { logAgentActivity(this, "Incorporating web search results into contract design", context2); contextInfo += "\n\nWEB SEARCH RESULTS:\n" + context2.webSearchResults.join("\n\n"); } if (context2.vectorSearchResults && context2.vectorSearchResults.length > 0) { logAgentActivity(this, "Incorporating documentation from vector search", context2); contextInfo += "\n\nDOCUMENTATION:\n" + context2.vectorSearchResults.join("\n\n"); } const contractMessages = [ { role: "system", content: `You are an expert Solidity developer tasked with creating secure, efficient, and well-documented smart contracts. Output only valid Solidity code without additional explanations. The contract should: - Use the most recent Solidity version (^0.8.20) - Be secure, following all best practices - Use appropriate OpenZeppelin contracts when relevant - Include comprehensive NatSpec documentation - Be gas-efficient - Include appropriate events, modifiers, and access control ${context2.options.proxy === "transparent" ? "\n- Implement upgradeability using OpenZeppelin TransparentUpgradeableProxy pattern and organise code in contracts/, proxy/, and scripts/ folders" : ""}${context2.options.proxy === "uups" ? "\n- Implement upgradeability using OpenZeppelin UUPSUpgradeable pattern and organise code in contracts/, proxy/, and scripts/ folders" : ""} ` }, { role: "user", content: [ `TASK: Create a Solidity smart contract that satisfies the following requirements:`, input, contextInfo ].filter(Boolean).join("\n\n") } ]; logAgentActivity(this, "Generating smart contract code...", context2); let contractCode = ""; if (context2.options.stream !== false) { const stream = await context2.openai.chat.completions.create({ model: context2.modelId, messages: contractMessages, stream: true }); logAgentActivity(this, "Writing code (streaming output)...", context2); for await (const chunk of stream) { const content_chunk = chunk.choices[0]?.delta?.content || ""; contractCode += content_chunk; logUpdate3(renderMarkdown(contractCode)); } logUpdate3.done(); } else { const completion = await context2.openai.chat.completions.create({ model: context2.modelId, messages: contractMessages }); contractCode = completion.choices[0].message.content || ""; console.log(renderMarkdown(contractCode)); } logAgentActivity(this, "Completed initial code generation", context2); context2.contractCode = contractCode; return contractCode; } }, // Linting Agent { name: "Linter", description: "Checks code for styling and best practices", emoji: "\u{1F9F9}", async execute(input, context2) { if (input) { logAgentActivity(this, "Linter input provided, using it to lint the code", context2); context2.contractCode = input; } else if (!context2.contractCode) { logAgentActivity(this, "No contract code available to lint", context2); return null; } logAgentActivity(this, "Performing linting checks on generated code", context2); const lintMessages = [ { role: "system", content: `You are a Solidity linting expert. Analyze the provided smart contract for: 1. Style inconsistencies 2. Non-adherence to Solidity style guides 3. Code organization issues 4. Naming convention violations 5. Comments and documentation issues For each issue found, provide the specific line/code and a suggested fix. If no issues are found in a category, mention that explicitly. ` }, { role: "user", content: context2.contractCode } ]; const lintingResponse = await context2.openai.chat.completions.create({ model: context2.modelId, messages: lintMessages }); const lintingResults = lintingResponse.choices[0].message.content; context2.lintingResults = lintingResults; logAgentActivity(this, "Linting complete, identified style and convention issues", context2); console.log(renderMarkdown(lintingResults)); if (lintingResults.toLowerCase().includes("issue") || lintingResults.toLowerCase().includes("violation") || lintingResults.toLowerCase().includes("inconsistenc")) { logAgentActivity(this, "Sending linting issues back to Code Writer for correction", context2); const fixMessages = [ { role: "system", content: `You are an expert Solidity developer. Fix the code based on the linting feedback. Return ONLY the fixed code without explanations or comments about the changes.` }, { role: "user", content: `Code: ${context2.contractCode} Linting feedback: ${lintingResults} Please fix all the issues.` } ]; const fixResponse = await context2.openai.chat.completions.create({ model: context2.modelId, messages: fixMessages }); const fixedCodeLLMResponse = fixResponse.choices[0].message.content; let potentialFixedLintedCode = stripMarkdownCodeBlocks(fixedCodeLLMResponse || ""); if (potentialFixedLintedCode && potentialFixedLintedCode.includes("pragma solidity") && potentialFixedLintedCode.length > 100) { context2.contractCode = potentialFixedLintedCode; logAgentActivity(this, "Code has been corrected based on linting feedback", context2); console.log(renderMarkdown(context2.contractCode)); } else { logAgentActivity(this, `Linter LLM failed to provide valid fixed code. Output: '${fixedCodeLLMResponse}'. Retaining previous code.`, context2); } return { lintingResults, fixedCode: potentialFixedLintedCode && potentialFixedLintedCode.includes("pragma solidity") && potentialFixedLintedCode.length > 100 ? context2.contractCode : void 0 }; } return lintingResults; } }, // Security Audit Agent { name: "Security Auditor", description: "Performs security audit of smart contract code", emoji: "\u{1F512}", async execute(input, context2) { if (input) { logAgentActivity(this, "Security audit input provided, using it to audit the code", context2); context2.contractCode = input; } else if (!context2.contractCode) { logAgentActivity(this, "No contract code available to audit", context2); return null; } logAgentActivity(this, "Beginning comprehensive security audit", context2); const securityMessages = [ { role: "system", content: `You are a smart contract security auditor specialized in identifying vulnerabilities and potential issues in Solidity code. Provide a security assessment that includes: 1. Identified vulnerabilities or security concerns 2. Recommendations for improvements 3. Best practices that should be followe