Skip to main content
Glama
setup-docker.ts8.2 kB
#!/usr/bin/env node import { program } from 'commander'; import * as fs from 'fs'; import * as path from 'path'; import * as bip39 from 'bip39'; import { randomBytes } from 'crypto'; import chalk from 'chalk'; import { execSync } from 'child_process'; program .name('setup-docker') .description('Set up a new agent for Docker deployment') .requiredOption('-a, --agent-id <id>', 'Agent ID (e.g., agent-123)') .option('-s, --seed <seed>', 'Wallet seed in hex format (if not provided, will be generated)') .option('-m, --mnemonic <words>', 'BIP39 mnemonic phrase (words separated by spaces)') .option('-f, --force', 'Overwrite existing seed file if it exists') .option('-w, --words <number>', 'number of words in mnemonic (12 or 24)', '24') .option('-p, --password <string>', 'optional password for additional security', '') .option('-P, --port <number>', 'Wallet server port', '3000') .option('-i, --indexer <url>', 'Indexer URL', 'http://indexer:8080') .option('-w, --indexer-ws <url>', 'Indexer WebSocket URL', 'ws://indexer:8080') .option('-n, --node <url>', 'Midnight node URL', 'http://midnight-node:8080') .option('--secure', 'Log seed information for secure copying') .parse(process.argv); const options = program.opts(); async function generateSeed(wordCount: number = 24, password: string = ''): Promise<{ seed: string; mnemonic: string; derivedSeed?: string }> { // Validate word count if (wordCount !== 12 && wordCount !== 24) { throw new Error('Word count must be either 12 or 24'); } // Generate strength based on word count (128 bits for 12 words, 256 bits for 24 words) const strength = wordCount === 12 ? 128 : 256; // Generate random entropy const entropyBytes = strength / 8; const entropy = randomBytes(entropyBytes); // Generate mnemonic from entropy const mnemonic = bip39.entropyToMnemonic(entropy); // For Midnight, we use the entropy as the seed (hex format) const seed = entropy.toString('hex'); // If password is provided, derive a seed from the mnemonic let derivedSeed: string | undefined; if (password) { const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, password); derivedSeed = seedBuffer.toString('hex'); } return { seed, mnemonic, derivedSeed }; } async function mnemonicToSeed(mnemonic: string, password: string = ''): Promise<{ seed: string; derivedSeed?: string }> { // Validate mnemonic if (!bip39.validateMnemonic(mnemonic)) { throw new Error('Invalid BIP39 mnemonic phrase'); } // Convert mnemonic back to entropy (this is the hex seed for Midnight) const entropy = bip39.mnemonicToEntropy(mnemonic); // For Midnight, we use the entropy as the seed const seed = entropy; // If password is provided, derive a seed from the mnemonic let derivedSeed: string | undefined; if (password) { const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, password); derivedSeed = seedBuffer.toString('hex'); } return { seed, derivedSeed }; } async function main() { try { const agentId = options.agentId; // Validate agent ID format if (!/^[a-zA-Z0-9-]+$/.test(agentId)) { throw new Error('Agent ID can only contain letters, numbers, and hyphens'); } // Create agents directory if it doesn't exist const agentsDir = path.join(process.cwd(), 'agents'); if (!fs.existsSync(agentsDir)) { console.log(chalk.cyan('Creating agents directory...')); fs.mkdirSync(agentsDir, { recursive: true }); } // Create agent directory const agentDir = path.join(agentsDir, agentId); if (!fs.existsSync(agentDir)) { console.log(chalk.cyan(`Creating directory for agent ${agentId}...`)); fs.mkdirSync(agentDir, { recursive: true }); } // Create data and logs directories const dataDir = path.join(agentDir, 'data'); const logsDir = path.join(agentDir, 'logs'); const seedsDir = path.join(dataDir, 'seeds', agentId); const backupsDir = path.join(dataDir, 'wallet-backups', agentId); const txDbDir = path.join(dataDir, 'transaction-db', agentId); // Create directories with proper permissions [dataDir, logsDir, seedsDir, backupsDir, txDbDir].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); // Generate seed file const seedPath = path.join(seedsDir, 'seed'); if (!fs.existsSync(seedPath) || options.force) { let seed: string; let mnemonic: string | undefined; let derivedSeed: string | undefined; if (options.seed) { // Use provided hex seed if (!/^[0-9a-fA-F]{64}$/.test(options.seed)) { throw new Error('Seed must be exactly 32 bytes (64 hex characters)'); } seed = options.seed; console.log(chalk.cyan('Using provided hex seed')); } else if (options.mnemonic) { // Convert mnemonic to seed const result = await mnemonicToSeed(options.mnemonic, options.password); seed = result.seed; derivedSeed = result.derivedSeed; mnemonic = options.mnemonic; console.log(chalk.cyan('Converting provided mnemonic to hex seed')); } else { // Generate new seed const result = await generateSeed(parseInt(options.words), options.password); seed = result.seed; mnemonic = result.mnemonic; derivedSeed = result.derivedSeed; console.log(chalk.cyan('Generating new random seed')); } fs.writeFileSync(seedPath, seed); fs.chmodSync(seedPath, 0o600); // Set read/write for owner only // Display seed information only if secure flag is provided if (options.secure) { console.log('\n=== Generated Wallet Information ==='); console.log(chalk.yellow('Midnight Seed (hex):')); console.log(chalk.white(seed)); if (mnemonic) { console.log('\n' + chalk.yellow('BIP39 Mnemonic:')); console.log(chalk.white(mnemonic)); } if (derivedSeed) { console.log('\n' + chalk.yellow('Derived Seed (with password):')); console.log(chalk.white(derivedSeed)); } } else { console.log(chalk.cyan('✓ Seed file created (use --secure flag to view seed information)')); } } // Create .env file const envPath = path.join(agentDir, '.env'); const envContent = `# Required AGENT_ID=${agentId} # Server Configuration WALLET_SERVER_PORT=${options.port || '3000'} # Network Configuration NETWORK_ID=TestNet LOG_LEVEL=info # External Services USE_EXTERNAL_PROOF_SERVER=true PROOF_SERVER=${options.proofServer} INDEXER=${options.indexer} INDEXER_WS=${options.indexerWs} MN_NODE=${options.node} `; fs.writeFileSync(envPath, envContent); console.log(chalk.green('✓ .env file created')); // Copy docker-compose.yml to agent directory const dockerComposePath = path.join(agentDir, 'docker-compose.yml'); if (!fs.existsSync(dockerComposePath)) { console.log(chalk.cyan('Copying docker-compose.yml...')); fs.copyFileSync(path.join(process.cwd(), 'docker-compose.yml'), dockerComposePath); } console.log('\n=== Docker Setup Instructions ==='); console.log(chalk.cyan('1. Change to the agent directory:')); console.log(chalk.white(` cd agents/${agentId}`)); console.log(chalk.cyan('\n2. Build and start the containers:')); console.log(chalk.white(' docker-compose up -d')); console.log(chalk.cyan('\n3. Check the logs:')); console.log(chalk.white(` docker-compose logs -f wallet-server`)); console.log(chalk.cyan('\n4. To stop the containers:')); console.log(chalk.white(' docker-compose down')); console.log(chalk.cyan('\n5. To remove all data (including volumes):')); console.log(chalk.white(' docker-compose down -v')); console.log('\nIMPORTANT: Keep your seed secure and never share it!'); console.log('Consider backing up your seed file securely.'); } catch (error) { console.error(chalk.red('\nError: Failed to set up Docker environment:')); console.error(chalk.red(error)); process.exit(1); } } main();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/evilpixi/pixi-midnight-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server