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/**/*"
+ ]
}