@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
JavaScript
#!/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