Skip to main content
Glama

SystemPrompt Coding Agent

Official
start-tunnel.ts9.81 kB
#!/usr/bin/env node /** * @file Start services with Cloudflare tunnel * @module scripts/start-tunnel * * @remarks * This script starts the MCP server with a public tunnel URL via Cloudflare */ import { spawn, execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { fileURLToPath } from 'url'; import * as dotenv from 'dotenv'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.join(__dirname, '..'); // Load environment variables from .env file dotenv.config({ path: path.join(projectRoot, '.env') }); // Colors for output const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' }; class TunnelStarter { private tunnelProcess: any = null; private tunnelUrl: string | null = null; private log(message: string, color: string = colors.reset): void { console.log(`${color}${message}${colors.reset}`); } private error(message: string): void { this.log(`❌ ${message}`, colors.red); } private success(message: string): void { this.log(`✅ ${message}`, colors.green); } private info(message: string): void { this.log(`ℹ️ ${message}`, colors.blue); } private warning(message: string): void { this.log(`⚠️ ${message}`, colors.yellow); } private checkCloudflared(): boolean { try { execSync('which cloudflared', { stdio: 'ignore' }); return true; } catch { return false; } } private getLocalNetworkAddresses(): string[] { const interfaces = os.networkInterfaces(); const addresses: string[] = []; for (const name of Object.keys(interfaces)) { const nets = interfaces[name]; if (!nets) continue; for (const net of nets) { // Skip internal (loopback) and non-IPv4 addresses if (!net.internal && net.family === 'IPv4') { addresses.push(net.address); } } } return addresses; } private checkClaudeHooks(): boolean { try { const homeDir = os.homedir(); const claudeSettingsPath = path.join(homeDir, '.config', 'claude', 'settings.json'); if (!fs.existsSync(claudeSettingsPath)) { return false; } const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); return settings.hooks && Object.keys(settings.hooks).length > 0; } catch { return false; } } async startTunnel(): Promise<string> { return new Promise((resolve, reject) => { const port = process.env.PORT || '3000'; this.info(`Starting Cloudflare tunnel on port ${port}...`); // Start cloudflared tunnel this.tunnelProcess = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], { stdio: ['ignore', 'pipe', 'pipe'] }); let output = ''; let errorOutput = ''; // Capture stdout this.tunnelProcess.stdout.on('data', (data: Buffer) => { const text = data.toString(); output += text; // Look for the tunnel URL in the output const urlMatch = text.match(/https:\/\/[^\s]+\.trycloudflare\.com/); if (urlMatch && !this.tunnelUrl) { this.tunnelUrl = urlMatch[0]; this.success(`Tunnel established: ${this.tunnelUrl}`); resolve(this.tunnelUrl); } }); // Capture stderr this.tunnelProcess.stderr.on('data', (data: Buffer) => { const text = data.toString(); errorOutput += text; // Cloudflared outputs URL to stderr sometimes const urlMatch = text.match(/https:\/\/[^\s]+\.trycloudflare\.com/); if (urlMatch && !this.tunnelUrl) { this.tunnelUrl = urlMatch[0]; this.success(`Tunnel established: ${this.tunnelUrl}`); resolve(this.tunnelUrl); } }); this.tunnelProcess.on('error', (error: Error) => { this.error(`Failed to start tunnel: ${error.message}`); reject(error); }); this.tunnelProcess.on('close', (code: number) => { if (code !== 0 && !this.tunnelUrl) { this.error(`Tunnel process exited with code ${code}`); reject(new Error(`Tunnel exited with code ${code}`)); } }); // Timeout after 30 seconds setTimeout(() => { if (!this.tunnelUrl) { this.error('Timeout waiting for tunnel URL'); if (this.tunnelProcess) { this.tunnelProcess.kill(); } reject(new Error('Timeout waiting for tunnel URL')); } }, 30000); }); } saveTunnelUrl(url: string): void { // Save to daemon logs directory const daemonLogsDir = path.join(projectRoot, 'daemon', 'logs'); fs.mkdirSync(daemonLogsDir, { recursive: true }); fs.writeFileSync(path.join(daemonLogsDir, 'tunnel-url.txt'), url); this.info(`Tunnel URL saved to: ${path.join(daemonLogsDir, 'tunnel-url.txt')}`); } updateEnvironment(url: string): void { // Update environment for child processes process.env.TUNNEL_URL = url; process.env.TUNNEL_ENABLED = 'true'; process.env.PUBLIC_URL = url; // Update .env file if it exists const envPath = path.join(projectRoot, '.env'); if (fs.existsSync(envPath)) { let envContent = fs.readFileSync(envPath, 'utf8'); // Add or update TUNNEL_URL if (envContent.includes('TUNNEL_URL=')) { envContent = envContent.replace(/TUNNEL_URL=.*/g, `TUNNEL_URL=${url}`); } else { envContent += `\n# Dynamically set by tunnel script\nTUNNEL_URL=${url}\n`; } fs.writeFileSync(envPath, envContent); this.info('Updated .env file with tunnel URL'); } } async startServices(): Promise<void> { this.info('Starting all services with tunnel enabled...'); // Run the start-all script directly const startAllPath = path.join(projectRoot, 'scripts', 'start-all.ts'); // Use spawn to run start-all with tunnel environment const startProcess = spawn('npx', ['tsx', startAllPath], { stdio: 'inherit', env: { ...process.env, TUNNEL_URL: this.tunnelUrl || '', TUNNEL_ENABLED: 'true', PUBLIC_URL: this.tunnelUrl || '' } }); startProcess.on('error', (error: Error) => { this.error(`Failed to start services: ${error.message}`); process.exit(1); }); startProcess.on('exit', (code: number | null) => { if (code !== 0) { this.error(`Start-all process exited with code ${code}`); process.exit(code || 1); } }); } async run(): Promise<void> { try { // Check if cloudflared is installed if (!this.checkCloudflared()) { this.error('cloudflared not found. Please run "npm run setup" first.'); process.exit(1); } // Check if Claude hooks are installed if (!this.checkClaudeHooks()) { this.warning('Claude hooks not configured!'); console.log(''); this.info('To enable Claude logging, run:'); console.log(` ${colors.cyan}./claude-hooks.sh install${colors.reset}`); console.log(''); this.info('Without hooks, Claude tool usage won\'t be logged to tasks.'); console.log(''); } else { this.success('Claude hooks are configured'); } // Start the tunnel const tunnelUrl = await this.startTunnel(); // Save tunnel URL this.saveTunnelUrl(tunnelUrl); // Update environment this.updateEnvironment(tunnelUrl); // Display tunnel info console.log('\n' + '='.repeat(60)); this.success('🌍 Your server is now accessible from the internet!'); this.info(`🔗 Public URL: ${colors.cyan}${tunnelUrl}${colors.reset}`); this.info(`📡 MCP Endpoint: ${colors.cyan}${tunnelUrl}/mcp${colors.reset}`); // Show create_log endpoint for hooks console.log(''); this.info('📝 Claude Hooks endpoint:'); console.log(` POST ${colors.cyan}${tunnelUrl}/tools/create_log${colors.reset}`); console.log('='.repeat(60) + '\n'); // Display local network info const localAddresses = this.getLocalNetworkAddresses(); const port = process.env.PORT || '3000'; if (localAddresses.length > 0) { console.log('\n' + '='.repeat(60)); this.info('🏠 Local network access (without tunnel):'); localAddresses.forEach(ip => { this.info(`📍 http://${ip}:${port}`); this.info(`📡 MCP Endpoint: http://${ip}:${port}/mcp`); }); console.log('='.repeat(60) + '\n'); } // Start services await this.startServices(); // Handle shutdown process.on('SIGINT', () => { this.warning('\nShutting down tunnel...'); if (this.tunnelProcess) { this.tunnelProcess.kill(); } // Clean up tunnel URL file const tunnelFile = path.join(projectRoot, 'daemon', 'logs', 'tunnel-url.txt'); if (fs.existsSync(tunnelFile)) { fs.unlinkSync(tunnelFile); } process.exit(0); }); } catch (error) { this.error(`Failed to start tunnel: ${error}`); process.exit(1); } } } // Main entry point if (process.argv[1] === fileURLToPath(import.meta.url)) { const starter = new TunnelStarter(); starter.run().catch(error => { console.error('Failed to start tunnel:', error); process.exit(1); }); } export { TunnelStarter };

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