Skip to main content
Glama

SystemPrompt Coding Agent

Official
setup.ts28.7 kB
#!/usr/bin/env tsx /** * @fileoverview SystemPrompt Coding Agent setup script * @module setup * @description Handles initial setup and configuration for the SystemPrompt Coding Agent, * including environment validation, dependency installation, and project building. */ import { execSync, exec, ExecSyncOptions } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import * as dotenv from 'dotenv'; import * as readline from 'readline'; import { promisify } from 'util'; const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.join(__dirname, '..'); /** * ANSI color codes for terminal output */ const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' } as const; /** * Supported platforms for the setup script */ type Platform = 'wsl' | 'macos' | 'windows' | 'linux'; /** * Represents an environment variable configuration */ interface EnvVariable { /** The name of the environment variable */ name: string; /** Human-readable description of the variable */ description: string; /** Default value if not set */ default?: string; } /** * Result of checking a CLI tool */ interface ToolCheckResult { /** Path to the tool executable */ path: string | null; /** Whether the tool is available and working */ available: boolean; /** Tool version if available */ version?: string; } /** * Required environment variables that must be configured */ const REQUIRED_ENV_VARS: readonly EnvVariable[] = [ { name: 'PROJECT_ROOT', description: 'Project root directory for task execution' } ] as const; /** * Optional environment variables with defaults */ const OPTIONAL_ENV_VARS: readonly EnvVariable[] = [ { name: 'PORT', default: '3000', description: 'MCP server port (Docker container external port)' }, { name: 'CLAUDE_PROXY_PORT', default: '9876', description: 'Host Bridge Daemon port for Claude proxy' }, { name: 'JWT_SECRET', description: 'JWT secret for authentication (optional)' }, { name: 'PUSH_TOKEN', description: 'Device push token for notifications (for mobile app)' }, { name: 'COMPOSE_PROJECT_NAME', default: 'systemprompt-coding-agent', description: 'Docker Compose project name' } ] as const; /** * Tool-related environment variables that are auto-detected */ const TOOL_ENV_VARS: readonly EnvVariable[] = [ { name: 'CLAUDE_PATH', description: 'Path to Claude CLI executable' }, { name: 'GEMINI_PATH', description: 'Path to Gemini CLI executable' }, { name: 'SHELL_PATH', default: '/bin/bash', description: 'Shell path for command execution' }, { name: 'CLAUDE_AVAILABLE', default: 'false', description: 'Whether Claude CLI is available' }, { name: 'GEMINI_AVAILABLE', default: 'false', description: 'Whether Gemini CLI is available' } ] as const; /** * Manages the complete setup process for the SystemPrompt Coding Agent */ class ProjectSetup { private readonly errors: string[] = []; private readonly rl: readline.Interface; constructor() { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout }); } /** * Logs a message with optional color * @param message - The message to log * @param color - ANSI color code */ private log(message: string, color: string = colors.reset): void { console.log(`${color}${message}${colors.reset}`); } /** * Logs an error message and tracks it * @param message - The error message */ private error(message: string): void { this.errors.push(message); console.error(`${colors.red}ERROR: ${message}${colors.reset}`); } /** * Logs a success message * @param message - The success message */ private success(message: string): void { this.log(`✓ ${message}`, colors.green); } /** * Logs an info message * @param message - The info message */ private info(message: string): void { this.log(`ℹ ${message}`, colors.blue); } /** * Logs a warning message * @param message - The warning message */ private warning(message: string): void { this.log(`⚠ ${message}`, colors.yellow); } /** * Logs a section header * @param title - The header title */ private header(title: string): void { this.log(`\n==== ${title} ====\n`, colors.green); } /** * Prompts the user for input * @param question - The question to ask * @returns The user's response */ private prompt(question: string): Promise<string> { return new Promise(resolve => { this.rl.question(question, resolve); }); } /** * Detects the current platform * @returns The detected platform */ private detectPlatform(): Platform { const platform = process.platform; if (platform === 'darwin') { return 'macos'; } if (platform === 'win32') { return 'windows'; } // Check if running in WSL try { const osRelease = fs.readFileSync('/proc/version', 'utf-8'); if (osRelease.toLowerCase().includes('microsoft')) { return 'wsl'; } } catch { // Not WSL } return 'linux'; } /** * Gets the appropriate host IP for Docker connectivity based on platform * @returns The host IP address */ private async getDockerHostIP(): Promise<string> { const platform = this.detectPlatform(); switch (platform) { case 'wsl': // In WSL2, we need the WSL2 VM's IP address try { const result = execSync('ip addr show eth0 | grep "inet " | awk \'{print $2}\' | cut -d/ -f1', { encoding: 'utf-8' }).trim(); if (result) { this.info(`Detected WSL2 host IP: ${result}`); return result; } } catch { this.warning('Could not detect WSL2 host IP, using default'); } return 'host.docker.internal'; case 'macos': case 'windows': // Docker Desktop provides host.docker.internal return 'host.docker.internal'; case 'linux': // Native Linux can use host network mode or bridge IP try { // Try to get the docker0 bridge IP const result = execSync('ip addr show docker0 | grep "inet " | awk \'{print $2}\' | cut -d/ -f1', { encoding: 'utf-8' }).trim(); if (result) { this.info(`Detected Docker bridge IP: ${result}`); return result; } } catch { // docker0 might not exist yet } return '172.17.0.1'; // Default Docker bridge gateway } } /** * Executes a shell command safely * @param command - The command to execute * @param options - Execution options * @returns The command output or null on failure */ private execCommandSafe(command: string, options: ExecSyncOptions = {}): string | null { try { const result = execSync(command, { encoding: 'utf-8', stdio: 'pipe', ...options }); return result.toString().trim(); } catch (error) { if (error && typeof error === 'object' && 'status' in error && error.status !== 0) { // Command failed with non-zero exit code return null; } throw error; // Re-throw actual errors } } /** * Executes a shell command with output * @param command - The command to execute * @param cwd - Working directory for the command * @returns Success status */ private execCommand(command: string, cwd?: string): boolean { try { execSync(command, { cwd: cwd || projectRoot, stdio: 'inherit', shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash' }); return true; } catch { return false; } } /** * Checks if a command exists in the system PATH * @param command - The command to check * @returns Path to the command or null if not found */ private checkCommand(command: string): string | null { try { const result = execSync( process.platform === 'win32' ? `where ${command}` : `which ${command}`, { encoding: 'utf-8', stdio: 'pipe' } ).trim(); return result || null; } catch { return null; } } /** * Validates the Node.js version * @returns Whether Node.js version meets requirements */ async checkNodeVersion(): Promise<boolean> { this.header('Checking Node.js version'); const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.split('.')[0].substring(1)); if (majorVersion < 18) { this.error(`Node.js v18+ required. Found: ${nodeVersion}`); this.info('Please upgrade Node.js: https://nodejs.org/'); return false; } this.success(`Node.js ${nodeVersion} found`); return true; } /** * Checks Docker and Docker Compose availability * @returns Whether Docker is properly installed */ async checkDocker(): Promise<boolean> { this.header('Checking Docker'); const dockerPath = this.checkCommand('docker'); if (!dockerPath) { this.error('Docker not found'); this.info('Install Docker from: https://docs.docker.com/get-docker/'); return false; } const composeVersion = this.execCommandSafe('docker compose version'); if (!composeVersion) { this.error('Docker Compose not working'); this.info('Ensure Docker Compose v2 is installed'); return false; } this.log(composeVersion); // Check if Docker daemon is running const dockerInfo = this.execCommandSafe('docker info', { timeout: 5000 }); if (!dockerInfo) { this.warning('Docker is installed but daemon is not running'); this.info('Docker Desktop will be started automatically when you run: npm start'); } else { this.success('Docker daemon is running'); } this.success('Docker and Docker Compose are available'); return true; } /** * Checks Claude CLI availability * @returns Claude CLI status */ async checkClaude(): Promise<ToolCheckResult> { this.header('Checking Claude CLI'); // First check for local claude in node_modules const isWindows = process.platform === 'win32'; const localClaudePath = path.join(projectRoot, 'node_modules/.bin', isWindows ? 'claude.cmd' : 'claude'); let claudePath: string | null = null; if (fs.existsSync(localClaudePath)) { claudePath = localClaudePath; } else { // Fall back to system PATH claudePath = this.checkCommand('claude'); } if (!claudePath) { this.warning('Claude CLI not found. Install from: https://github.com/anthropics/claude-cli'); this.updateToolEnvVar('CLAUDE_PATH', ''); this.updateToolEnvVar('CLAUDE_AVAILABLE', 'false'); return { path: null, available: false }; } this.updateToolEnvVar('CLAUDE_PATH', claudePath); // Check version const needsQuotes = process.platform === 'win32' || claudePath.includes(' '); const quotedPath = needsQuotes ? `"${claudePath}"` : claudePath; const version = this.execCommandSafe(`${quotedPath} --version`, { timeout: 5000 }); if (!version) { this.warning(`Claude found at ${claudePath} but not working properly`); this.updateToolEnvVar('CLAUDE_AVAILABLE', 'false'); return { path: claudePath, available: false }; } this.success(`Claude CLI found at: ${claudePath} (version: ${version})`); // Skip authentication check for now - it can be interactive // Just assume authenticated if version check worked this.info('Claude CLI is available (authentication check skipped)'); this.updateToolEnvVar('CLAUDE_AVAILABLE', 'true'); return { path: claudePath, available: true, version }; } /** * Updates a tool environment variable in .env file * @param name - Variable name * @param value - Variable value */ private updateToolEnvVar(name: string, value: string): void { const envPath = path.join(projectRoot, '.env'); if (!fs.existsSync(envPath)) { return; } let envContent = fs.readFileSync(envPath, 'utf-8'); const regex = new RegExp(`^${name}=.*$`, 'gm'); if (envContent.match(regex)) { envContent = envContent.replace(regex, `${name}=${value}`); } else { // Add in the tool section if it exists, otherwise at the end const toolSectionRegex = /^# Tool paths and availability.*$/gm; if (envContent.match(toolSectionRegex)) { // Add after the tool section header envContent = envContent.replace(toolSectionRegex, (match) => `${match}\n${name}=${value}`); } else { // Add at the end with a tool section envContent += `\n# Tool paths and availability (detected by setup/start scripts)\n${name}=${value}`; } } fs.writeFileSync(envPath, envContent); } /** * Checks Gemini CLI availability */ async checkGemini(): Promise<void> { this.header('Checking Gemini CLI'); const geminiPath = this.checkCommand('gemini'); if (!geminiPath) { this.info('Gemini CLI not found (optional)'); this.updateToolEnvVar('GEMINI_PATH', ''); this.updateToolEnvVar('GEMINI_AVAILABLE', 'false'); return; } this.updateToolEnvVar('GEMINI_PATH', geminiPath); const needsQuotes = process.platform === 'win32' || geminiPath.includes(' '); const quotedPath = needsQuotes ? `"${geminiPath}"` : geminiPath; const version = this.execCommandSafe(`${quotedPath} --version`, { timeout: 5000 }); if (!version) { this.warning(`Gemini found at ${geminiPath} but not working properly`); this.updateToolEnvVar('GEMINI_AVAILABLE', 'false'); return; } this.success(`Gemini CLI found at: ${geminiPath}`); this.updateToolEnvVar('GEMINI_AVAILABLE', 'true'); } /** * Checks Shell availability */ async checkShell(): Promise<void> { const shellPaths = ['/bin/bash', '/usr/bin/bash', 'bash']; let foundShell = ''; for (const shell of shellPaths) { const shellPath = this.checkCommand(shell); if (shellPath && fs.existsSync(shellPath)) { foundShell = shellPath; break; } } if (foundShell) { this.updateToolEnvVar('SHELL_PATH', foundShell); } else { this.warning('Bash shell not found, using default /bin/bash'); this.updateToolEnvVar('SHELL_PATH', '/bin/bash'); } } /** * Checks Cloudflare tunnel (cloudflared) availability * @returns Whether cloudflared is available */ async checkCloudflared(): Promise<boolean> { const envPath = path.join(projectRoot, '.env'); if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf-8'); const hasCloudflareToken = envContent.includes('CLOUDFLARE_TOKEN=') && !envContent.includes('CLOUDFLARE_TOKEN=your_cloudflare_tunnel_token_here'); if (!hasCloudflareToken) { return false; } } this.header('Checking Cloudflare Tunnel (cloudflared) - Optional'); const cloudflaredPath = this.checkCommand('cloudflared'); if (!cloudflaredPath) { this.info('Cloudflared not found (optional)'); this.info('If you want to use Cloudflare tunnels, install cloudflared manually:'); this.info('Visit: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'); return false; } else { this.success(`Cloudflared found at: ${cloudflaredPath}`); return true; } } /** * Creates required project directories */ async createDirectories(): Promise<void> { this.header('Creating project directories'); const dirs = ['logs', '.tmp', 'e2e-test/results', 'daemon/logs']; for (const dir of dirs) { const fullPath = path.join(projectRoot, dir); fs.mkdirSync(fullPath, { recursive: true }); } this.success('Directories created'); } /** * Installs project dependencies * @returns Success status */ async installDependencies(): Promise<boolean> { this.header('Installing project dependencies'); const projects = [ { name: 'Main', path: projectRoot }, { name: 'Daemon', path: path.join(projectRoot, 'daemon') }, { name: 'E2E test', path: path.join(projectRoot, 'e2e-test') } ]; for (const project of projects) { this.info(`Installing ${project.name} dependencies...`); if (!this.execCommand('npm install --no-audit --no-fund', project.path)) { this.error(`Failed to install ${project.name} dependencies`); return false; } this.success(`${project.name} dependencies installed`); } return true; } /** * Builds the TypeScript projects * @returns Success status */ async buildProject(): Promise<boolean> { this.header('Building TypeScript projects'); const buildTasks = [ { name: 'main project', command: 'npm run build', cwd: projectRoot }, { name: 'daemon', command: 'npm run build', cwd: path.join(projectRoot, 'daemon') }, { name: 'scripts', command: 'npm run build:scripts', cwd: projectRoot } ]; for (const task of buildTasks) { if (!this.execCommand(task.command, task.cwd)) { this.error(`Failed to build ${task.name}`); return false; } } this.success('All TypeScript projects built'); return true; } /** * Validates and configures environment variables * @returns Success status */ async checkEnvironmentFile(): Promise<boolean> { this.header('Checking environment configuration'); const envPath = path.join(projectRoot, '.env'); const envExamplePath = path.join(projectRoot, '.env.example'); if (!fs.existsSync(envPath)) { this.warning('.env file not found'); if (fs.existsSync(envExamplePath)) { fs.copyFileSync(envExamplePath, envPath); this.success('Created .env file from .env.example'); } else { this.error('.env.example not found'); return false; } } dotenv.config({ path: envPath }); let isValid = true; const envVars = dotenv.parse(fs.readFileSync(envPath)); this.info('\nValidating environment variables:'); // Check PROJECT_ROOT let projectRootValue: string | undefined = envVars['PROJECT_ROOT'] || process.env['PROJECT_ROOT']; if (!projectRootValue || projectRootValue.includes('/path/to/') || projectRootValue.includes('your_')) { const newValue = await this.promptForProjectRoot(projectRootValue); if (!newValue) { return false; } projectRootValue = newValue; this.updateEnvVar(envPath, 'PROJECT_ROOT', projectRootValue); this.success(`Updated PROJECT_ROOT in .env file: ${projectRootValue}`); dotenv.config({ path: envPath }); envVars['PROJECT_ROOT'] = projectRootValue; } else { this.success(` PROJECT_ROOT is set: ${projectRootValue}`); } // Check optional variables this.info('\nOptional environment variables:'); // Check for port conflicts await this.checkPortConflicts(envPath, envVars); // Check and set CLAUDE_PROXY_HOST based on platform await this.configureDockerHost(envPath, envVars); // Display status of optional variables for (const { name, default: defaultValue } of OPTIONAL_ENV_VARS) { const value = envVars[name] || process.env[name]; if (!value || value.includes('your_') || value.includes('_here')) { this.info(` ${name} is not set (optional)`); if (defaultValue) { this.info(` Will use default: ${defaultValue}`); } } else { this.success(` ${name} is set`); } } return isValid; } /** * Prompts for PROJECT_ROOT configuration * @param currentValue - Current value if any * @returns The configured path or null */ private async promptForProjectRoot(currentValue: string | undefined): Promise<string | null> { this.warning('PROJECT_ROOT is not set or contains placeholder value'); this.info('PROJECT_ROOT is the directory where Claude Code will execute tasks'); const suggestedPath = process.cwd(); const answer = await this.prompt(`\nEnter PROJECT_ROOT path (default: ${suggestedPath}): `); const projectRootValue = answer || suggestedPath; if (!fs.existsSync(projectRootValue)) { this.error(`Path does not exist: ${projectRootValue}`); const create = await this.prompt('Would you like to create this directory? (y/N): '); if (create.toLowerCase() === 'y') { try { fs.mkdirSync(projectRootValue, { recursive: true }); this.success(`Created directory: ${projectRootValue}`); } catch (err) { this.error(`Failed to create directory: ${err}`); return null; } } else { return null; } } return projectRootValue; } /** * Updates an environment variable in the .env file * @param envPath - Path to .env file * @param name - Variable name * @param value - Variable value */ private updateEnvVar(envPath: string, name: string, value: string): void { let envContent = fs.readFileSync(envPath, 'utf-8'); const regex = new RegExp(`^${name}=.*$`, 'gm'); if (envContent.match(regex)) { envContent = envContent.replace(regex, `${name}=${value}`); } else { envContent += `\n# ${REQUIRED_ENV_VARS.find(v => v.name === name)?.description || name}\n${name}=${value}\n`; } fs.writeFileSync(envPath, envContent); } /** * Checks for port conflicts and suggests alternatives * @param envPath - Path to .env file * @param envVars - Current environment variables */ private async checkPortConflicts(envPath: string, envVars: Record<string, string>): Promise<void> { const portsToCheck = [ { name: 'PORT', default: '3000' }, { name: 'CLAUDE_PROXY_PORT', default: '9876' } ]; for (const { name, default: defaultValue } of portsToCheck) { const currentValue = envVars[name] || defaultValue; const portInUse = await this.isPortInUse(parseInt(currentValue)); if (portInUse) { this.warning(` ${name}=${currentValue} - Port is already in use!`); const suggestedPort = parseInt(currentValue) + 1; const useAlt = await this.prompt(` Would you like to use port ${suggestedPort} instead? (y/N): `); if (useAlt.toLowerCase() === 'y') { this.updateEnvVar(envPath, name, suggestedPort.toString()); this.success(` Updated ${name} to ${suggestedPort}`); } } } } /** * Checks if a port is in use * @param port - Port number to check * @returns Whether the port is in use */ private async isPortInUse(port: number): Promise<boolean> { try { const { stdout } = await execAsync( process.platform === 'darwin' ? `lsof -i :${port} -t 2>/dev/null || true` : `netstat -tlnp 2>/dev/null | grep :${port} 2>/dev/null || true` ); return stdout.trim().length > 0; } catch { return false; } } /** * Configures Docker host IP based on platform * @param envPath - Path to .env file * @param envVars - Current environment variables */ private async configureDockerHost(envPath: string, envVars: Record<string, string>): Promise<void> { const claudeProxyHost = envVars['CLAUDE_PROXY_HOST']; if (!claudeProxyHost || claudeProxyHost === 'host.docker.internal') { const platform = this.detectPlatform(); const hostIP = await this.getDockerHostIP(); if (platform === 'wsl' && hostIP !== 'host.docker.internal') { this.info(`\n Detected WSL2 environment`); this.info(` Setting CLAUDE_PROXY_HOST to WSL2 host IP: ${hostIP}`); let envContent = fs.readFileSync(envPath, 'utf-8'); const regex = /^CLAUDE_PROXY_HOST=.*$/gm; if (envContent.match(regex)) { envContent = envContent.replace(regex, `CLAUDE_PROXY_HOST=${hostIP}`); } else { const portRegex = /^CLAUDE_PROXY_PORT=.*$/gm; if (envContent.match(portRegex)) { envContent = envContent.replace(portRegex, (match) => `${match}\n\n# Claude proxy host for Docker (auto-detected for WSL2)\nCLAUDE_PROXY_HOST=${hostIP}` ); } else { envContent += `\n# Claude proxy host for Docker (auto-detected for WSL2)\nCLAUDE_PROXY_HOST=${hostIP}\n`; } } fs.writeFileSync(envPath, envContent); this.success(` Updated CLAUDE_PROXY_HOST for WSL2 compatibility`); } else { this.info(` CLAUDE_PROXY_HOST will use default: ${hostIP}`); } } } /** * Runs the complete setup process */ async run(): Promise<void> { this.log('🚀 SystemPrompt Coding Agent Setup', colors.cyan); this.log('==================================', colors.cyan); try { // Critical checks if (!await this.checkNodeVersion()) { process.exit(1); } const dockerAvailable = await this.checkDocker(); if (!dockerAvailable) { this.error('\nSetup cannot continue without Docker'); process.exit(1); } const envValid = await this.checkEnvironmentFile(); if (!envValid) { this.error('\nSetup incomplete: Failed to configure environment'); process.exit(1); } // Tool checks const claude = await this.checkClaude(); await this.checkGemini(); await this.checkShell(); const cloudflaredAvailable = await this.checkCloudflared(); // Project setup await this.createDirectories(); if (!await this.installDependencies()) { this.error('Failed to install dependencies'); process.exit(1); } if (!await this.buildProject()) { this.error('Failed to build projects'); process.exit(1); } // Display completion message this.displayCompletionMessage(claude, cloudflaredAvailable); } finally { this.rl.close(); } } /** * Displays the completion message with next steps * @param claude - Claude tool check result * @param cloudflaredAvailable - Whether cloudflared is available */ private displayCompletionMessage(claude: ToolCheckResult, cloudflaredAvailable: boolean): void { this.log('\n✨ Setup completed successfully!', colors.green); if (this.errors.length > 0) { this.warning('\nSome issues were encountered:'); this.errors.forEach(err => this.error(` - ${err}`)); } this.log('\nNext steps:', colors.cyan); this.info('1. Review and update .env file if needed'); this.info('2. Start the server: npm run start'); this.info('3. Run tests: npm test'); this.info('\nUseful commands:'); this.success(' npm start # Start all services'); this.success(' npm run tunnel # Start services with internet tunnel'); this.success(' npm test # Run all tests'); this.success(' npm run logs # View logs'); this.success(' npm run status # Check service status'); if (!claude.available) { this.warning('\nNote: Claude CLI is not available.'); this.info('Install Claude CLI for full functionality: npm install -g @anthropic-ai/claude-code'); } if (cloudflaredAvailable) { this.info('\n✨ Cloudflare tunnel is available! Use "npm run tunnel" to expose your server.'); } else { this.info('\nCloudflare tunnel not configured. To use tunnels:'); this.info('1. Set CLOUDFLARE_TOKEN in .env'); this.info('2. Install cloudflared from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'); } this.info('\nFor more information, see README.md'); } } // Execute setup when run directly if (process.argv[1] === fileURLToPath(import.meta.url)) { const setup = new ProjectSetup(); setup.run().catch(error => { console.error('Setup failed:', error); process.exit(1); }); }

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/systempromptio/systemprompt-code-orchestrator'

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