diff --git a/.gitignore b/.gitignore index ac666dd..cb5c0c1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,4 @@ anvil.log .env # Local deployment files -cli/deployments/localhost.json \ No newline at end of file +cli/deployments/devnet.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..60f65b1 --- /dev/null +++ b/.npmignore @@ -0,0 +1,52 @@ +# Source files (only compiled JS will be included) +*.ts +!*.d.ts + +# Tests +tests/ +**/*.test.ts +**/*.test.js + +# Build config +tsconfig.json +tsconfig.build.json +bun.lockb + +# Development +.env +.env.* +!.env.example + +# Logs +*.log +anvil.log +.anvil.pid + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# CI/CD +.github/ + +# Documentation (keep README.md) +docs/ + +# Deployments +deployments/ + +# Node modules (will be installed by user) +node_modules/ + +# Parent directory references +../ diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..e8f4067 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,173 @@ +# Installing NLA CLI Globally + +## Prerequisites + +- Node.js >= 18.0.0 +- npm or yarn or pnpm +- A blockchain node (local Anvil or remote RPC URL) +- At least one LLM provider API key (OpenAI, Anthropic, or OpenRouter) + +## Installation + +### Global Installation (Recommended) + +Install the NLA CLI globally to use it from anywhere: + +```bash +npm install -g nla +``` + +Or with yarn: +```bash +yarn global add nla +``` + +Or with pnpm: +```bash +pnpm add -g nla +``` + +### Verify Installation + +```bash +nla --help +``` + +## Configuration + +Create a `.env` file in your project directory: + +```bash +# Copy the example environment file +cp node_modules/nla/.env.example .env + +# Edit with your configuration +nano .env +``` + +Required environment variables: +```bash +# At least one LLM provider API key +OPENAI_API_KEY=sk-... +# OR +ANTHROPIC_API_KEY=sk-ant-... +# OR +OPENROUTER_API_KEY=sk-or-... + +# Oracle configuration +ORACLE_PRIVATE_KEY=0x... +RPC_URL=http://localhost:8545 + +# Optional: For enhanced search +PERPLEXITY_API_KEY=pplx-... +``` + +## Quick Start + +### 1. Start Development Environment + +```bash +nla dev +``` + +This will: +- Start Anvil (local blockchain) +- Deploy all contracts +- Start the oracle + +### 2. Create an Escrow + +```bash +nla escrow:create \ + --demand "The sky is blue" \ + --amount 10 \ + --token 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853 \ + --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +``` + +### 3. Fulfill an Escrow + +```bash +nla escrow:fulfill \ + --escrow-uid 0x... \ + --fulfillment "The sky appears blue today" \ + --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +``` + +### 4. Collect Payment + +```bash +nla escrow:collect \ + --escrow-uid 0x... \ + --fulfillment-uid 0x... +``` + +## Uninstallation + +```bash +npm uninstall -g nla +``` + +## Development + +If you want to contribute or modify the CLI: + +```bash +# Clone the repository +git clone https://github.com/arkhai-io/natural-language-agreements.git +cd natural-language-agreements + +# Install dependencies +npm install + +# Build +npm run build + +# Link locally +npm link +``` + +## Troubleshooting + +### Command not found after installation + +Make sure your npm global bin directory is in your PATH: + +```bash +# Check npm global bin path +npm bin -g + +# Add to PATH (add to ~/.bashrc or ~/.zshrc) +export PATH="$(npm bin -g):$PATH" +``` + +### Permission errors on Linux/Mac + +If you get permission errors, either: + +1. Use a Node version manager (recommended): +```bash +# Install nvm +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash + +# Install and use Node +nvm install 18 +nvm use 18 + +# Now install without sudo +npm install -g nla +``` + +2. Or configure npm to use a different directory: +```bash +mkdir ~/.npm-global +npm config set prefix '~/.npm-global' +echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc +source ~/.bashrc +``` + +## Support + +For issues and questions: +- GitHub: https://github.com/arkhai-io/natural-language-agreements/issues +- Documentation: https://github.com/arkhai-io/natural-language-agreements diff --git a/README.md b/README.md index dadc439..801021f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ export OPENAI_API_KEY=sk-your-key-here nla deploy ``` -This creates `cli/deployments/localhost.json` with all contract addresses. +This creates `cli/deployments/devnet.json` with all contract addresses. #### 3. Start Oracle @@ -316,7 +316,7 @@ natural-language-agreements/ │ │ ├── start-oracle.sh # Oracle starter │ │ └── stop.sh # Cleanup script │ └── deployments/ # Deployment addresses (generated) -│ ├── localhost.json +│ ├── devnet.json │ ├── sepolia.json │ └── mainnet.json ├── nla.ts # Natural Language Agreement client library diff --git a/cli/README.md b/cli/README.md index 5a1a2bb..034fd48 100644 --- a/cli/README.md +++ b/cli/README.md @@ -99,7 +99,7 @@ Monitor the escrow and arbitration progress: ```bash nla escrow:status \ --escrow-uid 0x... \ - --deployment ./cli/deployments/localhost.json + --deployment ./cli/deployments/devnet.json ``` This will show: @@ -185,7 +185,7 @@ Options: --token
ERC20 token address (required) --oracle
Oracle address (required) --private-key Your private key (or set PRIVATE_KEY env var) - --deployment Deployment file (default: ./cli/deployments/localhost.json) + --deployment Deployment file (default: ./cli/deployments/devnet.json) --rpc-url RPC URL (default: from deployment) --help, -h Show help ``` @@ -200,7 +200,7 @@ Options: --fulfillment Your fulfillment text (required) --oracle
Oracle address (required) --private-key Your private key (or set PRIVATE_KEY env var) - --deployment Deployment file (default: ./cli/deployments/localhost.json) + --deployment Deployment file (default: ./cli/deployments/devnet.json) --rpc-url RPC URL (default: from deployment) --help, -h Show help ``` @@ -214,7 +214,7 @@ Options: --escrow-uid Escrow UID (required) --fulfillment-uid Approved fulfillment UID (required) --private-key Your private key (or set PRIVATE_KEY env var) - --deployment Deployment file (default: ./cli/deployments/localhost.json) + --deployment Deployment file (default: ./cli/deployments/devnet.json) --rpc-url RPC URL (default: from deployment) --help, -h Show help ``` @@ -226,7 +226,7 @@ nla escrow:status [options] Options: --escrow-uid Escrow UID to check (required) - --deployment Deployment file (default: ./cli/deployments/localhost.json) + --deployment Deployment file (default: ./cli/deployments/devnet.json) --rpc-url RPC URL (default: from deployment) --help, -h Show help ``` @@ -293,7 +293,7 @@ nla escrow:fulfill \ # Terminal 2: Check the status nla escrow:status \ --escrow-uid 0xd9e1402e96c2f7a64e60bf53a45445f7254e9b72389f6ede25181bff542d7b65 \ - --deployment ./cli/deployments/localhost.json + --deployment ./cli/deployments/devnet.json # Terminal 2: Collect the escrow (as Bob) nla escrow:collect \ diff --git a/cli/client/collect-escrow.ts b/cli/client/collect-escrow.ts index 3746fa3..5cad394 100644 --- a/cli/client/collect-escrow.ts +++ b/cli/client/collect-escrow.ts @@ -8,10 +8,43 @@ import { parseArgs } from "util"; import { createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { foundry } from "viem/chains"; import { existsSync, readFileSync } from "fs"; -import { resolve } from "path"; +import { resolve, dirname, join } from "path"; +import { fileURLToPath } from "url"; import { makeClient } from "alkahest-ts"; +import { getChainFromNetwork } from "../utils.js"; + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Helper function to find deployment file +function findDeploymentFile(deploymentPath: string): string | null { + // Try the provided path first + if (existsSync(resolve(deploymentPath))) { + return resolve(deploymentPath); + } + + // Try relative to current working directory + const cwdPath = resolve(process.cwd(), deploymentPath); + if (existsSync(cwdPath)) { + return cwdPath; + } + + // Try relative to the CLI installation directory + const cliPath = resolve(__dirname, "..", "deployments", "devnet.json"); + if (existsSync(cliPath)) { + return cliPath; + } + + // Try in the project root (for local development) + const projectPath = resolve(__dirname, "..", "..", "cli", "deployments", "devnet.json"); + if (existsSync(projectPath)) { + return projectPath; + } + + return null; +} // Helper function to display usage function displayHelp() { @@ -27,7 +60,7 @@ Options: --escrow-uid Escrow UID to collect (required) --fulfillment-uid Fulfillment UID that was approved (required) --private-key Your private key (required) - --deployment Path to deployment file (default: ./cli/deployments/localhost.json) + --deployment Path to deployment file (default: ./cli/deployments/devnet.json) --rpc-url RPC URL (default: from deployment file) --help, -h Display this help message @@ -80,7 +113,7 @@ async function main() { const escrowUid = args["escrow-uid"]; const fulfillmentUid = args["fulfillment-uid"]; const privateKey = args["private-key"] || process.env.PRIVATE_KEY; - const deploymentPath = args.deployment || "./cli/deployments/localhost.json"; + const deploymentPath = args.deployment || "./cli/deployments/devnet.json"; // Validate required parameters if (!escrowUid) { @@ -102,14 +135,20 @@ async function main() { } // Load deployment file - if (!existsSync(resolve(deploymentPath))) { + const resolvedDeploymentPath = findDeploymentFile(deploymentPath); + if (!resolvedDeploymentPath) { console.error(`❌ Error: Deployment file not found: ${deploymentPath}`); console.error("Please deploy contracts first or specify correct path with --deployment"); + console.error("\nSearched in:"); + console.error(` - ${resolve(deploymentPath)}`); + console.error(` - ${resolve(process.cwd(), deploymentPath)}`); + console.error(` - ${resolve(__dirname, "..", "deployments", "devnet.json")}`); process.exit(1); } - const deployment = JSON.parse(readFileSync(resolve(deploymentPath), "utf-8")); + const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8")); const rpcUrl = args["rpc-url"] || deployment.rpcUrl; + const chain = getChainFromNetwork(deployment.network); console.log("🚀 Collecting Natural Language Agreement Escrow\n"); console.log("Configuration:"); @@ -121,7 +160,7 @@ async function main() { const account = privateKeyToAccount(privateKey as `0x${string}`); const walletClient = createWalletClient({ account, - chain: foundry, + chain, transport: http(rpcUrl), }).extend(publicActions); diff --git a/cli/client/create-escrow.ts b/cli/client/create-escrow.ts index 0ac0731..c705c11 100644 --- a/cli/client/create-escrow.ts +++ b/cli/client/create-escrow.ts @@ -9,20 +9,64 @@ import { parseArgs } from "util"; import { createWalletClient, http, publicActions, parseEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { foundry } from "viem/chains"; import { existsSync, readFileSync } from "fs"; -import { resolve } from "path"; +import { resolve, dirname, join } from "path"; +import { fileURLToPath } from "url"; import { makeClient } from "alkahest-ts"; import { makeLLMClient } from "../.."; import {fixtures} from "alkahest-ts"; +import { getCurrentEnvironment } from "../commands/switch.js"; +import { getChainFromNetwork } from "../utils.js"; + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Helper function to find deployment file +function findDeploymentFile(deploymentPath: string, environment?: string): string | null { + // If no path provided, use current environment + if (!deploymentPath) { + const env = environment || getCurrentEnvironment(); + deploymentPath = `./cli/deployments/${env}.json`; + } + + // Try the provided path first + if (existsSync(resolve(deploymentPath))) { + return resolve(deploymentPath); + } + + // Try relative to current working directory + const cwdPath = resolve(process.cwd(), deploymentPath); + if (existsSync(cwdPath)) { + return cwdPath; + } + + // Try relative to the CLI installation directory with current environment + const env = environment || getCurrentEnvironment(); + const cliPath = resolve(__dirname, "..", "deployments", `${env}.json`); + if (existsSync(cliPath)) { + return cliPath; + } + + // Try in the project root (for local development) + const projectPath = resolve(__dirname, "..", "..", "cli", "deployments", `${env}.json`); + if (existsSync(projectPath)) { + return projectPath; + } + + return null; +} // Helper function to display usage function displayHelp() { + const currentEnv = getCurrentEnvironment(); console.log(` Natural Language Agreement Escrow CLI Create an escrow with a natural language demand that will be arbitrated by an oracle. +Current environment: ${currentEnv} + Usage: bun cli/create-escrow.ts [options] @@ -32,7 +76,7 @@ Options: --token
ERC20 token address (required) --oracle
Oracle address that will arbitrate (required) --private-key Your private key (required) - --deployment Path to deployment file (default: ./cli/deployments/localhost.json) + --deployment Path to deployment file (default: current environment) --rpc-url RPC URL (default: from deployment file) --arbitration-provider Arbitration provider (default: OpenAI) --arbitration-model Arbitration model (default: gpt-4o-mini) @@ -97,7 +141,7 @@ async function main() { const tokenAddress = args.token; const oracleAddress = args.oracle; const privateKey = args["private-key"] || process.env.PRIVATE_KEY; - const deploymentPath = args.deployment || "./cli/deployments/localhost.json"; + const deploymentPath = args.deployment || `./cli/deployments/${getCurrentEnvironment()}.json`; // Arbitration configuration with defaults const arbitrationProvider = args["arbitration-provider"] || "OpenAI"; @@ -141,14 +185,20 @@ Fulfillment: {{obligation}}`; } // Load deployment file - if (!existsSync(resolve(deploymentPath))) { + const resolvedDeploymentPath = findDeploymentFile(deploymentPath); + if (!resolvedDeploymentPath) { console.error(`❌ Error: Deployment file not found: ${deploymentPath}`); console.error("Please deploy contracts first or specify correct path with --deployment"); + console.error("\nSearched in:"); + console.error(` - ${resolve(deploymentPath)}`); + console.error(` - ${resolve(process.cwd(), deploymentPath)}`); + console.error(` - ${resolve(__dirname, "..", "deployments", "devnet.json")}`); process.exit(1); } - const deployment = JSON.parse(readFileSync(resolve(deploymentPath), "utf-8")); + const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8")); const rpcUrl = args["rpc-url"] || deployment.rpcUrl; + const chain = getChainFromNetwork(deployment.network); console.log("🚀 Creating Natural Language Agreement Escrow\n"); console.log("Configuration:"); @@ -156,13 +206,14 @@ Fulfillment: {{obligation}}`; console.log(` 💰 Amount: ${amount} tokens`); console.log(` 🪙 Token: ${tokenAddress}`); console.log(` ⚖️ Oracle: ${oracleAddress}`); + console.log(` 🌐 Network: ${deployment.network}`); console.log(` 🌐 RPC URL: ${rpcUrl}\n`); // Create account and wallet const account = privateKeyToAccount(privateKey as `0x${string}`); const walletClient = createWalletClient({ account, - chain: foundry, + chain, transport: http(rpcUrl), }).extend(publicActions); diff --git a/cli/client/fulfill-escrow.ts b/cli/client/fulfill-escrow.ts index 9a72570..1b8e073 100644 --- a/cli/client/fulfill-escrow.ts +++ b/cli/client/fulfill-escrow.ts @@ -9,11 +9,44 @@ import { parseArgs } from "util"; import { createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { foundry } from "viem/chains"; import { existsSync, readFileSync } from "fs"; -import { resolve } from "path"; +import { resolve, dirname, join } from "path"; +import { fileURLToPath } from "url"; import { makeClient } from "alkahest-ts"; import { makeLLMClient } from "../.."; +import { getChainFromNetwork } from "../utils.js"; + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Helper function to find deployment file +function findDeploymentFile(deploymentPath: string): string | null { + // Try the provided path first + if (existsSync(resolve(deploymentPath))) { + return resolve(deploymentPath); + } + + // Try relative to current working directory + const cwdPath = resolve(process.cwd(), deploymentPath); + if (existsSync(cwdPath)) { + return cwdPath; + } + + // Try relative to the CLI installation directory + const cliPath = resolve(__dirname, "..", "deployments", "devnet.json"); + if (existsSync(cliPath)) { + return cliPath; + } + + // Try in the project root (for local development) + const projectPath = resolve(__dirname, "..", "..", "cli", "deployments", "devnet.json"); + if (existsSync(projectPath)) { + return projectPath; + } + + return null; +} // Helper function to display usage function displayHelp() { @@ -30,7 +63,7 @@ Options: --fulfillment Your fulfillment text (required) --oracle
Oracle address that will arbitrate (required) --private-key Your private key (required) - --deployment Path to deployment file (default: ./cli/deployments/localhost.json) + --deployment Path to deployment file (default: ./cli/deployments/devnet.json) --rpc-url RPC URL (default: from deployment file) --help, -h Display this help message @@ -86,7 +119,7 @@ async function main() { const fulfillment = args.fulfillment; const oracleAddress = args.oracle; const privateKey = args["private-key"] || process.env.PRIVATE_KEY; - const deploymentPath = args.deployment || "./cli/deployments/localhost.json"; + const deploymentPath = args.deployment || "./cli/deployments/devnet.json"; // Validate required parameters if (!escrowUid) { @@ -114,27 +147,34 @@ async function main() { } // Load deployment file - if (!existsSync(resolve(deploymentPath))) { + const resolvedDeploymentPath = findDeploymentFile(deploymentPath); + if (!resolvedDeploymentPath) { console.error(`❌ Error: Deployment file not found: ${deploymentPath}`); console.error("Please deploy contracts first or specify correct path with --deployment"); + console.error("\nSearched in:"); + console.error(` - ${resolve(deploymentPath)}`); + console.error(` - ${resolve(process.cwd(), deploymentPath)}`); + console.error(` - ${resolve(__dirname, "..", "deployments", "devnet.json")}`); process.exit(1); } - const deployment = JSON.parse(readFileSync(resolve(deploymentPath), "utf-8")); + const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8")); const rpcUrl = args["rpc-url"] || deployment.rpcUrl; + const chain = getChainFromNetwork(deployment.network); console.log("🚀 Fulfilling Natural Language Agreement Escrow\n"); console.log("Configuration:"); console.log(` 📦 Escrow UID: ${escrowUid}`); console.log(` 📝 Fulfillment: "${fulfillment}"`); console.log(` ⚖️ Oracle: ${oracleAddress}`); + console.log(` 🌐 Network: ${deployment.network}`); console.log(` 🌐 RPC URL: ${rpcUrl}\n`); // Create account and wallet const account = privateKeyToAccount(privateKey as `0x${string}`); const walletClient = createWalletClient({ account, - chain: foundry, + chain, transport: http(rpcUrl), }).extend(publicActions); diff --git a/cli/commands/dev.ts b/cli/commands/dev.ts new file mode 100644 index 0000000..0df804f --- /dev/null +++ b/cli/commands/dev.ts @@ -0,0 +1,250 @@ +import { spawn, spawnSync } from "child_process"; +import { existsSync, readFileSync, writeFileSync, createWriteStream, unlinkSync } from "fs"; +import { join } from "path"; + +// Colors for console output +const colors = { + green: '\x1b[32m', + blue: '\x1b[34m', + red: '\x1b[31m', + yellow: '\x1b[33m', + reset: '\x1b[0m' +}; + +// Load .env file if it exists +function loadEnvFile(envPath: string = '.env') { + if (existsSync(envPath)) { + console.log(`${colors.blue}📄 Loading .env file from: ${envPath}${colors.reset}`); + const envContent = readFileSync(envPath, 'utf-8'); + const lines = envContent.split('\n'); + + for (const line of lines) { + // Skip comments and empty lines + if (line.trim().startsWith('#') || !line.trim()) continue; + + // Parse key=value + const match = line.match(/^([^=]+)=(.*)$/); + if (match) { + const key = match[1].trim(); + const value = match[2].trim(); + // Only set if not already in environment + if (!process.env[key]) { + process.env[key] = value; + } + } + } + console.log(`${colors.green}✅ Environment variables loaded${colors.reset}`); + } else if (envPath !== '.env') { + // Only error if a custom path was specified but not found + console.error(`${colors.red}❌ .env file not found at: ${envPath}${colors.reset}`); + process.exit(1); + } +} + +// Helper to check if a command exists +function commandExists(command: string): boolean { + try { + const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [command], { + stdio: 'pipe' + }); + return result.status === 0; + } catch { + return false; + } +} + +// Helper to check if a port is in use +function isPortInUse(port: number): boolean { + try { + const result = spawnSync('lsof', ['-Pi', `:${port}`, '-sTCP:LISTEN', '-t'], { + stdio: 'pipe' + }); + return result.status === 0; + } catch { + return false; + } +} + +// Dev command - Start complete development environment +export async function runDevCommand(cliDir: string, envPath?: string) { + console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}`); + console.log(`${colors.blue} Natural Language Agreement Oracle - Quick Setup${colors.reset}`); + console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}\n`); + + // Load .env file first + loadEnvFile(envPath); + console.log(''); + + // Check prerequisites + console.log(`${colors.blue}📋 Checking prerequisites...${colors.reset}\n`); + + // Check Bun + if (!commandExists('bun')) { + console.error(`${colors.red}❌ Bun is not installed${colors.reset}`); + console.log('Please install it: https://bun.sh'); + process.exit(1); + } + console.log(`${colors.green}✅ Bun installed${colors.reset}`); + + // Check Foundry + if (!commandExists('forge')) { + console.error(`${colors.red}❌ Foundry (forge) is not installed${colors.reset}`); + console.log('Please install it: https://book.getfoundry.sh/getting-started/installation'); + process.exit(1); + } + console.log(`${colors.green}✅ Foundry installed${colors.reset}`); + + // Check Anvil + if (!commandExists('anvil')) { + console.error(`${colors.red}❌ Anvil is not installed${colors.reset}`); + console.log('Please install Foundry: https://book.getfoundry.sh/getting-started/installation'); + process.exit(1); + } + console.log(`${colors.green}✅ Anvil installed${colors.reset}`); + + // Check LLM API keys + const hasOpenAI = !!process.env.OPENAI_API_KEY; + const hasAnthropic = !!process.env.ANTHROPIC_API_KEY; + const hasOpenRouter = !!process.env.OPENROUTER_API_KEY; + const hasPerplexity = !!process.env.PERPLEXITY_API_KEY; + + if (!hasOpenAI && !hasAnthropic && !hasOpenRouter && !hasPerplexity) { + console.error(`${colors.red}❌ No LLM provider API key set${colors.reset}`); + console.log('Please add at least one API key to your environment:'); + console.log(' export OPENAI_API_KEY=sk-...'); + console.log(' export ANTHROPIC_API_KEY=sk-ant-...'); + console.log(' export OPENROUTER_API_KEY=sk-or-...'); + process.exit(1); + } + + if (hasOpenAI) console.log(`${colors.green}✅ OpenAI API key configured${colors.reset}`); + if (hasAnthropic) console.log(`${colors.green}✅ Anthropic API key configured${colors.reset}`); + if (hasOpenRouter) console.log(`${colors.green}✅ OpenRouter API key configured${colors.reset}`); + if (hasPerplexity) console.log(`${colors.green}✅ Perplexity API key configured${colors.reset}`); + console.log(''); + + // Check if Anvil is already running + if (isPortInUse(8545)) { + console.log(`${colors.yellow}⚠️ Anvil is already running on port 8545${colors.reset}`); + console.log(`${colors.blue}Using existing Anvil instance${colors.reset}\n`); + } else { + // Start Anvil + console.log(`${colors.blue}🔨 Starting Anvil...${colors.reset}`); + const anvilProcess = spawn('anvil', [], { + stdio: ['ignore', 'pipe', 'pipe'], + detached: true + }); + + // Save PID + writeFileSync('.anvil.pid', anvilProcess.pid!.toString()); + + // Redirect output to log file + const logStream = createWriteStream('anvil.log', { flags: 'a' }); + anvilProcess.stdout?.pipe(logStream); + anvilProcess.stderr?.pipe(logStream); + + anvilProcess.unref(); + + console.log(`${colors.green}✅ Anvil started (PID: ${anvilProcess.pid})${colors.reset}`); + console.log(' Logs: tail -f anvil.log'); + + // Wait for Anvil to be ready + await new Promise(resolve => setTimeout(resolve, 3000)); + } + + // Deploy contracts + console.log(`\n${colors.blue}📝 Deploying contracts...${colors.reset}\n`); + const deployScript = join(cliDir, 'server', 'deploy.js'); + const deployResult = spawnSync('bun', ['run', deployScript, '--network', 'localhost', '--rpc-url', 'http://localhost:8545'], { + stdio: 'inherit', + cwd: process.cwd() + }); + + if (deployResult.status !== 0) { + console.error(`${colors.red}❌ Deployment failed${colors.reset}`); + process.exit(1); + } + + // Start oracle + console.log(`\n${colors.blue}🚀 Starting oracle...${colors.reset}\n`); + const oracleScript = join(cliDir, 'server', 'oracle.js'); + + // Look for deployment file in source directory (for local dev) or dist directory (for installed package) + const sourcePath = join(process.cwd(), 'cli', 'deployments', 'devnet.json'); + const distPath = join(cliDir, 'deployments', 'devnet.json'); + const deploymentFile = existsSync(sourcePath) ? sourcePath : distPath; + + const oracleProcess = spawn('bun', ['run', oracleScript, '--deployment', deploymentFile], { + stdio: 'inherit', + cwd: process.cwd() + }); + + // Handle cleanup on exit + const cleanup = () => { + console.log(`\n${colors.yellow}🛑 Shutting down...${colors.reset}`); + + // Kill oracle + oracleProcess.kill(); + + // Kill Anvil + try { + const pidFile = '.anvil.pid'; + if (existsSync(pidFile)) { + const pid = readFileSync(pidFile, 'utf-8').trim(); + try { + process.kill(parseInt(pid)); + console.log(`${colors.green}✅ Anvil stopped${colors.reset}`); + } catch (e) { + // Process might already be dead + } + unlinkSync(pidFile); + } + } catch (e) { + // Ignore errors + } + + process.exit(0); + }; + + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + + // Open a new terminal + console.log(`\n${colors.green}✅ Setup complete!${colors.reset}`); + console.log(`${colors.blue}Opening new terminal...${colors.reset}\n`); + + try { + // For macOS, open a new Terminal window + if (process.platform === 'darwin') { + spawn('osascript', [ + '-e', + `tell application "Terminal" to do script "cd ${process.cwd()}"` + ], { detached: true, stdio: 'ignore' }).unref(); + } + // For Linux, try common terminal emulators + else if (process.platform === 'linux') { + const terminals = ['gnome-terminal', 'konsole', 'xterm']; + for (const term of terminals) { + if (commandExists(term)) { + spawn(term, ['--working-directory', process.cwd()], { + detached: true, + stdio: 'ignore' + }).unref(); + break; + } + } + } + // For Windows + else if (process.platform === 'win32') { + spawn('cmd', ['/c', 'start', 'cmd', '/K', `cd /d ${process.cwd()}`], { + detached: true, + stdio: 'ignore' + }).unref(); + } + } catch (e) { + console.log(`${colors.yellow}⚠️ Could not open new terminal automatically${colors.reset}`); + } + + // Keep process alive + await new Promise(() => {}); +} diff --git a/cli/commands/stop.ts b/cli/commands/stop.ts new file mode 100644 index 0000000..4c4328b --- /dev/null +++ b/cli/commands/stop.ts @@ -0,0 +1,43 @@ +import { spawnSync } from "child_process"; +import { existsSync, readFileSync, unlinkSync } from "fs"; + +// Colors for console output +const colors = { + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', + reset: '\x1b[0m' +}; + +// Stop command - Stop all services +export async function runStopCommand() { + console.log(`${colors.yellow}🛑 Stopping services...${colors.reset}\n`); + + // Stop Anvil + try { + const pidFile = '.anvil.pid'; + if (existsSync(pidFile)) { + const pid = readFileSync(pidFile, 'utf-8').trim(); + try { + process.kill(parseInt(pid)); + console.log(`${colors.green}✅ Anvil stopped${colors.reset}`); + } catch (e) { + console.log(`${colors.yellow}⚠️ Anvil process not found (may have already stopped)${colors.reset}`); + } + unlinkSync(pidFile); + } else { + console.log(`${colors.yellow}⚠️ No Anvil PID file found${colors.reset}`); + } + } catch (e) { + console.error(`${colors.red}❌ Error stopping Anvil:${colors.reset}`, e); + } + + // Try to kill any remaining processes on port 8545 + try { + spawnSync('pkill', ['-f', 'anvil']); + } catch (e) { + // Ignore + } + + console.log(`\n${colors.green}✅ Services stopped${colors.reset}`); +} diff --git a/cli/commands/switch.ts b/cli/commands/switch.ts new file mode 100644 index 0000000..6ae360d --- /dev/null +++ b/cli/commands/switch.ts @@ -0,0 +1,127 @@ +import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; +import { join, dirname } from "path"; +import { homedir } from "os"; +import { fileURLToPath } from "url"; + +// Get the directory of the current module +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Colors for console output +const colors = { + green: '\x1b[32m', + blue: '\x1b[34m', + red: '\x1b[31m', + yellow: '\x1b[33m', + reset: '\x1b[0m' +}; + +// Get NLA config directory +function getNLAConfigDir(): string { + const configDir = join(homedir(), '.nla'); + if (!existsSync(configDir)) { + mkdirSync(configDir, { recursive: true }); + } + return configDir; +} + +// Get current environment +export function getCurrentEnvironment(): string { + const configPath = join(getNLAConfigDir(), 'config.json'); + + if (!existsSync(configPath)) { + // Default to devnet + return 'devnet'; + } + + try { + const config = JSON.parse(readFileSync(configPath, 'utf-8')); + return config.environment || 'devnet'; + } catch (e) { + return 'devnet'; + } +} + +// Set current environment +function setCurrentEnvironment(env: string): void { + const configPath = join(getNLAConfigDir(), 'config.json'); + const config = existsSync(configPath) + ? JSON.parse(readFileSync(configPath, 'utf-8')) + : {}; + + config.environment = env; + writeFileSync(configPath, JSON.stringify(config, null, 2)); +} + +// Get deployment path for environment +export function getDeploymentPath(cliDir: string, env?: string): string { + const environment = env || getCurrentEnvironment(); + const filename = `${environment}.json`; + + // Try multiple locations + const paths = [ + join(cliDir, 'deployments', filename), // dist/cli/deployments/ + join(__dirname, '..', 'deployments', filename), // Relative to switch.ts + join(process.cwd(), 'cli', 'deployments', filename), // Project root + ]; + + for (const path of paths) { + if (existsSync(path)) { + return path; + } + } + + // Return the first path as default (even if it doesn't exist yet) + return paths[0]; +} + +// Switch command +export function runSwitchCommand(env?: string) { + console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}`); + console.log(`${colors.blue} Natural Language Agreement - Environment Switch${colors.reset}`); + console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}\n`); + + // If no environment specified, show current + if (!env) { + const current = getCurrentEnvironment(); + console.log(`${colors.blue}Current environment:${colors.reset} ${colors.green}${current}${colors.reset}\n`); + console.log('Available environments:'); + console.log(' • devnet (local Anvil blockchain)'); + console.log(' • sepolia (Ethereum Sepolia testnet)'); + console.log(' • mainnet (Ethereum mainnet)\n'); + console.log(`${colors.yellow}Usage:${colors.reset} nla switch `); + console.log(`${colors.yellow}Example:${colors.reset} nla switch sepolia\n`); + return; + } + + // Validate environment + const validEnvs = ['devnet', 'sepolia', 'mainnet']; + if (!validEnvs.includes(env)) { + console.error(`${colors.red}❌ Invalid environment: ${env}${colors.reset}`); + console.log('Valid environments: devnet, sepolia, mainnet\n'); + process.exit(1); + } + + // Switch environment + const currentEnv = getCurrentEnvironment(); + + if (currentEnv === env) { + console.log(`${colors.yellow}⚠️ Already on ${env}${colors.reset}\n`); + return; + } + + setCurrentEnvironment(env); + console.log(`${colors.green}✅ Switched to ${env}${colors.reset}\n`); + + // Show info about the environment + if (env === 'devnet') { + console.log('📝 Using local Anvil blockchain (http://localhost:8545)'); + console.log(' Run "nla dev" to start the development environment\n'); + } else if (env === 'sepolia') { + console.log('📝 Using Ethereum Sepolia testnet'); + console.log(' Make sure you have deployed contracts and updated sepolia.json\n'); + } else if (env === 'mainnet') { + console.log('📝 Using Ethereum mainnet'); + console.log(` ${colors.yellow}⚠️ WARNING: This is production! Use with caution.${colors.reset}\n`); + } +} diff --git a/cli/deployments/sepolia.json b/cli/deployments/sepolia.json new file mode 100644 index 0000000..1a0a6a8 --- /dev/null +++ b/cli/deployments/sepolia.json @@ -0,0 +1,50 @@ +{ + "network": "sepolia", + "chainId": 11155111, + "rpcUrl": "https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_KEY", + "addresses": { + "eas": "", + "easSchemaRegistry": "", + "erc20EscrowObligation": "", + "erc20PaymentObligation": "", + "erc20BarterUtils": "", + "erc721EscrowObligation": "", + "erc721PaymentObligation": "", + "erc721BarterUtils": "", + "erc1155EscrowObligation": "", + "erc1155BarterUtils": "", + "erc1155PaymentObligation": "", + "tokenBundleEscrowObligation": "", + "tokenBundlePaymentObligation": "", + "tokenBundleBarterUtils": "", + "attestationEscrowObligation": "", + "attestationEscrowObligation2": "", + "attestationBarterUtils": "", + "stringObligation": "", + "trivialArbiter": "", + "trustedOracleArbiter": "", + "anyArbiter": "", + "allArbiter": "", + "intrinsicsArbiter": "", + "intrinsicsArbiter2": "", + "exclusiveRevocableConfirmationArbiter": "", + "exclusiveUnrevocableConfirmationArbiter": "", + "nonexclusiveRevocableConfirmationArbiter": "", + "nonexclusiveUnrevocableConfirmationArbiter": "", + "nativeTokenEscrowObligation": "", + "nativeTokenPaymentObligation": "", + "nativeTokenBarterUtils": "", + "recipientArbiter": "", + "attesterArbiter": "", + "schemaArbiter": "", + "uidArbiter": "", + "refUidArbiter": "", + "revocableArbiter": "", + "timeAfterArbiter": "", + "timeBeforeArbiter": "", + "timeEqualArbiter": "", + "expirationTimeAfterArbiter": "", + "expirationTimeBeforeArbiter": "", + "expirationTimeEqualArbiter": "" + } +} diff --git a/cli/index.ts b/cli/index.ts index e50beff..b675be4 100755 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,11 +1,21 @@ -#!/usr/bin/env bun +#!/usr/bin/env node import { parseArgs } from "util"; import { spawnSync } from "child_process"; import { existsSync, readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; import { createPublicClient, http, parseAbiParameters, decodeAbiParameters } from "viem"; import { foundry } from "viem/chains"; import { contracts } from "alkahest-ts"; + +import { runDevCommand } from "./commands/dev.js"; +import { runStopCommand } from "./commands/stop.js"; +import { runSwitchCommand } from "./commands/switch.js"; + +// Get the directory name for ESM modules (compatible with both Node and Bun) +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); // Helper function to display usage function displayHelp() { console.log(` @@ -19,6 +29,8 @@ Commands: deploy Deploy contracts to blockchain start-oracle Start the oracle service stop Stop all services (Anvil + Oracle) + switch [env] Switch between environments (devnet, sepolia, mainnet) + network Show current network/environment escrow:create Create a new escrow with natural language demand escrow:fulfill Fulfill an existing escrow escrow:collect Collect an approved escrow @@ -39,6 +51,7 @@ Options (vary by command): --arbitration-provider Arbitration provider (create, default: OpenAI) --arbitration-model Arbitration model (create, default: gpt-4o-mini) --arbitration-prompt Custom arbitration prompt (create, optional) + --env Path to .env file (dev, default: .env) Environment Variables: PRIVATE_KEY Private key for transactions @@ -49,6 +62,9 @@ Examples: # Start development environment nla dev + # Start development with custom .env file + nla dev --env /path/to/.env.production + # Deploy contracts nla deploy @@ -83,7 +99,7 @@ Examples: // Parse command line arguments function parseCliArgs() { - const args = Bun.argv.slice(2); + const args = process.argv.slice(2); if (args.length === 0) { displayHelp(); @@ -113,34 +129,19 @@ function parseCliArgs() { "arbitration-provider": { type: "string" }, "arbitration-model": { type: "string" }, "arbitration-prompt": { type: "string" }, + "env": { type: "string" }, + "environment": { type: "string" }, }, - strict: true, + strict: command !== "switch" && command !== "network", // Allow positional args for switch command + allowPositionals: command === "switch" || command === "network", }); return { command, ...values }; } -// Shell command handler -async function runShellCommand(scriptName: string, args: string[] = []) { - const { spawnSync } = await import("child_process"); - const scriptDir = import.meta.dir; - const scriptPath = `${scriptDir}/scripts/${scriptName}`; - - // Run the shell script - const result = spawnSync(scriptPath, args, { - stdio: "inherit", - cwd: process.cwd(), - shell: true, - }); - - process.exit(result.status || 0); -} - // Server command handler (for deploy.ts, oracle.ts) async function runServerCommand(scriptName: string, args: string[] = []) { - const { spawnSync } = await import("child_process"); - const scriptDir = import.meta.dir; - const scriptPath = `${scriptDir}/server/${scriptName}`; + const scriptPath = join(__dirname, "server", scriptName); // Run the TypeScript file directly const result = spawnSync("bun", ["run", scriptPath, ...args], { @@ -157,25 +158,38 @@ async function main() { const args = parseCliArgs(); const command = args.command; - // Handle shell script commands (dev and stop need shell for process management) + // Handle dev and stop commands if (command === "dev") { - await runShellCommand("dev.sh"); + await runDevCommand(__dirname, args.env as string | undefined); return; } if (command === "stop") { - await runShellCommand("stop.sh"); + await runStopCommand(); + return; + } + + if (command === "switch") { + // Get environment from either --environment flag or second positional arg + const env = args.environment as string | undefined || process.argv[3]; + runSwitchCommand(env); + return; + } + + if (command === "network") { + // Show current network (same as switch with no args) + runSwitchCommand(); return; } // Handle TypeScript commands that can run directly if (command === "deploy") { - await runServerCommand("deploy.ts", Bun.argv.slice(3)); + await runServerCommand("deploy.js", process.argv.slice(3)); return; } if (command === "start-oracle") { - await runServerCommand("oracle.ts", Bun.argv.slice(3)); + await runServerCommand("oracle.js", process.argv.slice(3)); return; } @@ -184,13 +198,13 @@ async function main() { switch (command) { case "escrow:create": - scriptPath = "./client/create-escrow.ts"; + scriptPath = "./client/create-escrow.js"; break; case "escrow:fulfill": - scriptPath = "./client/fulfill-escrow.ts"; + scriptPath = "./client/fulfill-escrow.js"; break; case "escrow:collect": - scriptPath = "./client/collect-escrow.ts"; + scriptPath = "./client/collect-escrow.js"; break; case "escrow:status": await runStatusCommand(args); @@ -203,11 +217,10 @@ async function main() { // Run the command as a subprocess with the args (excluding the command name) const { spawnSync } = await import("child_process"); - const scriptDir = import.meta.dir; - const fullScriptPath = `${scriptDir}/${scriptPath}`; + const fullScriptPath = join(__dirname, scriptPath); // Build args array without the command name - const commandArgs = Bun.argv.slice(3); // Skip bun, script, and command + const commandArgs = process.argv.slice(3); // Skip node, script, and command const result = spawnSync("bun", ["run", fullScriptPath, ...commandArgs], { stdio: "inherit", diff --git a/cli/scripts/dev.sh b/cli/scripts/dev.sh deleted file mode 100755 index cfa0cea..0000000 --- a/cli/scripts/dev.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/bash - -# Complete setup and deployment for local development -# This script will: -# 1. Check prerequisites -# 2. Start Anvil -# 3. Deploy contracts -# 4. Start the oracle - -set -e - -# Colors for output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo -e "${BLUE}════════════════════════════════════════════════════════${NC}" -echo -e "${BLUE} Natural Language Agreement Oracle - Quick Setup${NC}" -echo -e "${BLUE}════════════════════════════════════════════════════════${NC}\n" - -# Check prerequisites -echo -e "${BLUE}📋 Checking prerequisites...${NC}\n" - -# Load .env file if it exists -if [ -f ".env" ]; then - echo -e "${BLUE}📄 Loading .env file...${NC}" - export $(cat .env | grep -v '^#' | xargs) - echo -e "${GREEN}✅ Environment variables loaded${NC}" -fi - -# Check Bun -if ! command -v bun &> /dev/null; then - echo -e "${RED}❌ Bun is not installed${NC}" - echo "Please install it: https://bun.sh" - exit 1 -fi -echo -e "${GREEN}✅ Bun installed${NC}" - -# Check Foundry -if ! command -v forge &> /dev/null; then - echo -e "${RED}❌ Foundry (forge) is not installed${NC}" - echo "Please install it: https://book.getfoundry.sh/getting-started/installation" - exit 1 -fi -echo -e "${GREEN}✅ Foundry installed${NC}" - -# Check Anvil -if ! command -v anvil &> /dev/null; then - echo -e "${RED}❌ Anvil is not installed${NC}" - echo "Please install Foundry: https://book.getfoundry.sh/getting-started/installation" - exit 1 -fi -echo -e "${GREEN}✅ Anvil installed${NC}" - -# Check alkahest -if [ ! -d "../alkahest" ]; then - echo -e "${RED}❌ alkahest repository not found${NC}" - echo "Please clone it in the parent directory:" - echo " cd .. && git clone https://github.com/arkhai-io/alkahest.git" - exit 1 -fi -echo -e "${GREEN}✅ alkahest repository found${NC}" - -# Check LLM API keys -if [ -z "$OPENAI_API_KEY" ] && [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$OPENROUTER_API_KEY" ]; then - echo -e "${RED}❌ No LLM provider API key set${NC}" - echo "Please add at least one API key to your .env file:" - echo " OPENAI_API_KEY=sk-..." - echo " ANTHROPIC_API_KEY=sk-ant-..." - echo " OPENROUTER_API_KEY=sk-or-..." - exit 1 -fi - -if [ -n "$OPENAI_API_KEY" ]; then - echo -e "${GREEN}✅ OpenAI API key configured${NC}" -fi -if [ -n "$ANTHROPIC_API_KEY" ]; then - echo -e "${GREEN}✅ Anthropic API key configured${NC}" -fi -if [ -n "$OPENROUTER_API_KEY" ]; then - echo -e "${GREEN}✅ OpenRouter API key configured${NC}" -fi -if [ -n "$PERPLEXITY_API_KEY" ]; then - echo -e "${GREEN}✅ Perplexity API key configured${NC}" -fi -echo "" - -# Install dependencies -echo -e "${BLUE}📦 Installing dependencies...${NC}\n" -bun install - -# Check if Anvil is already running -if lsof -Pi :8545 -sTCP:LISTEN -t >/dev/null ; then - echo -e "${YELLOW}⚠️ Anvil is already running on port 8545${NC}" - read -p "Do you want to kill it and start fresh? (y/n) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - pkill -f anvil || true - sleep 2 - else - echo -e "${BLUE}Using existing Anvil instance${NC}\n" - fi -fi - -# Start Anvil in background if not running -if ! lsof -Pi :8545 -sTCP:LISTEN -t >/dev/null ; then - echo -e "${BLUE}🔨 Starting Anvil...${NC}" - anvil > anvil.log 2>&1 & - ANVIL_PID=$! - echo $ANVIL_PID > .anvil.pid - echo -e "${GREEN}✅ Anvil started (PID: $ANVIL_PID)${NC}" - echo " Logs: tail -f anvil.log" - sleep 3 -fi - -# Deploy contracts -echo -e "\n${BLUE}📝 Deploying contracts...${NC}\n" -bun run cli/server/deploy.ts --network localhost --rpc-url http://localhost:8545 - -# Start oracle -echo -e "\n${BLUE}🚀 Starting oracle...${NC}\n" -bun run cli/server/oracle.ts --deployment ./cli/deployments/localhost.json - -# Cleanup function -cleanup() { - echo -e "\n${YELLOW}🛑 Shutting down...${NC}" - if [ -f .anvil.pid ]; then - ANVIL_PID=$(cat .anvil.pid) - kill $ANVIL_PID 2>/dev/null || true - rm .anvil.pid - echo -e "${GREEN}✅ Anvil stopped${NC}" - fi - exit 0 -} - -trap cleanup SIGINT SIGTERM diff --git a/cli/scripts/stop.sh b/cli/scripts/stop.sh deleted file mode 100755 index 7536c5d..0000000 --- a/cli/scripts/stop.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -# Stop all running oracle and Anvil processes - -# Colors for output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo -e "${BLUE}🛑 Stopping Natural Language Agreement services...${NC}\n" - -# Stop Anvil -if [ -f .anvil.pid ]; then - ANVIL_PID=$(cat .anvil.pid) - if kill -0 $ANVIL_PID 2>/dev/null; then - kill $ANVIL_PID - echo -e "${GREEN}✅ Stopped Anvil (PID: $ANVIL_PID)${NC}" - fi - rm .anvil.pid -else - # Try to kill any running anvil - pkill -f anvil && echo -e "${GREEN}✅ Stopped Anvil${NC}" || echo -e "${YELLOW}⚠️ No Anvil process found${NC}" -fi - -# Stop oracle -pkill -f "bun run oracle" && echo -e "${GREEN}✅ Stopped oracle${NC}" || echo -e "${YELLOW}⚠️ No oracle process found${NC}" - -echo -e "\n${GREEN}✨ Cleanup complete${NC}" diff --git a/cli/server/deploy.ts b/cli/server/deploy.ts index 092adb5..66b3b3f 100644 --- a/cli/server/deploy.ts +++ b/cli/server/deploy.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env bun +#!/usr/bin/env node /** * Deployment script for Alkahest Natural Language Agreement Oracle * @@ -54,7 +54,7 @@ Examples: // Parse command line arguments function parseCliArgs() { const { values } = parseArgs({ - args: Bun.argv.slice(2), + args: process.argv.slice(2), options: { "network": { type: "string" }, "rpc-url": { type: "string" }, @@ -329,7 +329,10 @@ async function main() { // Get the script directory and go up to project root, then into cli/deployments const scriptDir = import.meta.dir; const projectRoot = resolve(scriptDir, "../.."); - const outputPath = args.output || resolve(projectRoot, `cli/deployments/${network}.json`); + + // Map localhost to devnet for file naming + const deploymentFileName = network === "localhost" ? "devnet" : network; + const outputPath = args.output || resolve(projectRoot, `cli/deployments/${deploymentFileName}.json`); const outputDir = resolve(outputPath, ".."); if (!existsSync(outputDir)) { diff --git a/cli/server/oracle.ts b/cli/server/oracle.ts index 57926b0..92d2cbb 100644 --- a/cli/server/oracle.ts +++ b/cli/server/oracle.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env bun +#!/usr/bin/env node import { parseArgs } from "util"; import { parseAbiParameters, createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; @@ -44,7 +44,7 @@ Examples: bun oracle.ts --rpc-url http://localhost:8545 --private-key 0x... --openai-api-key sk-... # Using deployment file - bun oracle.ts --deployment ./deployments/localhost.json --private-key 0x... --openai-api-key sk-... + bun oracle.ts --deployment ./deployments/devnet.json --private-key 0x... --openai-api-key sk-... # Using environment variables export OPENAI_API_KEY=sk-... @@ -57,7 +57,7 @@ Examples: // Parse command line arguments function parseCliArgs() { const { values } = parseArgs({ - args: Bun.argv.slice(2), + args: process.argv.slice(2), options: { "rpc-url": { type: "string" }, "private-key": { type: "string" }, diff --git a/cli/utils.ts b/cli/utils.ts new file mode 100644 index 0000000..0a96f36 --- /dev/null +++ b/cli/utils.ts @@ -0,0 +1,23 @@ +/** + * Shared utilities for NLA CLI + */ + +import { foundry, sepolia, mainnet } from "viem/chains"; +import type { Chain } from "viem/chains"; + +/** + * Get viem chain configuration from network name + */ +export function getChainFromNetwork(network: string): Chain { + switch (network.toLowerCase()) { + case "localhost": + case "devnet": + return foundry; + case "sepolia": + return sepolia; + case "mainnet": + return mainnet; + default: + return foundry; + } +} diff --git a/package.json b/package.json index 4134f9a..bc44fce 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,50 @@ { - "name": "natural-language-agreement-extension", - "module": "index.ts", + "name": "nla", + "version": "1.0.3", + "description": "Natural Language Agreement Oracle - CLI for creating and managing blockchain agreements using natural language", "type": "module", - "private": true, - "devDependencies": { - "@types/bun": "latest" + "private": false, + "author": "Arkhai", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/arkhai-io/natural-language-agreements.git" }, + "keywords": [ + "blockchain", + "ethereum", + "smart-contracts", + "natural-language", + "oracle", + "ai", + "llm", + "escrow" + ], "bin": { - "nla": "cli/index.ts" + "nla": "dist/cli/index.js" + }, + "files": [ + "dist/**/*", + "README.md", + ".env.example" + ], + "engines": { + "node": ">=18.0.0" + }, + "preferGlobal": true, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^20.0.0", + "typescript": "^5.9.3" }, "scripts": { + "build": "tsc --project tsconfig.build.json && chmod +x dist/cli/index.js", + "prepublishOnly": "npm run build", + "patch": "npm version patch", + "minor": "npm version minor", + "major": "npm version major", "dev": "bun run index.ts", + "public": "npm publish --access public", "start": "bun run index.ts", "test": "bun test ./tests --exclude alkahest-ts/** ", "setup": "bun run cli/index.ts dev", diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..00fba9a --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true + }, + "include": ["cli/**/*"], + "exclude": ["node_modules", "tests", "dist"] +} diff --git a/tsconfig.json b/tsconfig.json index cc0c50e..49c508d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,30 +1,16 @@ { "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "outDir": "dist", + "rootDir": ".", + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices + "esModuleInterop": true, "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, - - } + "skipLibCheck": true + }, + "include": [ + "cli/**/*" + ] }