diff --git a/.env.example b/.env.example index a7e9528..d4b83f1 100755 --- a/.env.example +++ b/.env.example @@ -1,5 +1,15 @@ # Claude Code UI Environment Configuration # Only includes variables that are actually used in the code +# +# TIP: Run 'cloudcli status' to see where this file should be located +# and to view your current configuration. +# +# Available CLI commands: +# claude-code-ui - Start the server (default) +# cloudcli start - Start the server +# cloudcli status - Show configuration and data locations +# cloudcli help - Show help information +# cloudcli version - Show version information # ============================================================================= # SERVER CONFIGURATION @@ -19,10 +29,11 @@ VITE_PORT=5173 # ============================================================================= # Path to the authentication database file -# This should be set to a persistent volume path when running in containers -# Default: server/database/auth.db (relative to project root) -# Example for Docker: /data/auth.db -# DATABASE_PATH=/data/auth.db +# This is where user credentials, API keys, and tokens are stored. +# +# To use a custom location: +# DATABASE_PATH=/path/to/your/custom/auth.db +# # Claude Code context window size (maximum tokens per session) # Note: VITE_ prefix makes it available to frontend VITE_CONTEXT_WINDOW=160000 diff --git a/README.md b/README.md index c94709d..1f81fd0 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,7 @@ npx @siteboon/claude-code-ui The server will start and be accessible at `http://localhost:3001` (or your configured PORT). -**To restart**: Simply run the same `npx` command again after stopping the server (Ctrl+C or Cmd+C). - +**To restart**: Simply run the same `npx` command again after stopping the server ### Global Installation (For Regular Use) For frequent use, install globally once: @@ -85,32 +84,71 @@ Then start with a simple command: claude-code-ui ``` -**Benefits**: -- Faster startup (no download/cache check) -- Simple command to remember -- Same experience every time **To restart**: Stop with Ctrl+C and run `claude-code-ui` again. -### Run as Background Service (Optional) +### CLI Commands -To keep the server running in the background, use PM2: +After global installation, you have access to both `claude-code-ui` and `cloudcli` commands: ```bash -# Install PM2 globally (one-time) -npm install -g pm2 +# Start the server (default command) +claude-code-ui +cloudcli start -# Start the server -pm2 start claude-code-ui --name "claude-ui" +# Show configuration and data locations +cloudcli status -# Manage the service -pm2 list # View status -pm2 restart claude-ui # Restart -pm2 stop claude-ui # Stop -pm2 logs claude-ui # View logs -pm2 startup # Auto-start on system boot +# Show help information +cloudcli help + +# Show version +cloudcli version ``` +**The `cloudcli status` command shows you:** +- Installation directory location +- Database location (where credentials are stored) +- Current configuration (PORT, DATABASE_PATH, etc.) +- Claude projects folder location +- Configuration file location + +``` + +### Run as Background Service (Recommended for Production) + +For production use, run Claude Code UI as a background service using PM2 (Process Manager 2): + +#### Install PM2 + +```bash +npm install -g pm2 +``` + +#### Start as Background Service + +```bash +# Start the server in background +pm2 start claude-code-ui --name "claude-code-ui" + +# Or using the shorter alias +pm2 start cloudcli --name "claude-code-ui" +``` + + +#### Auto-Start on System Boot + +To make Claude Code UI start automatically when your system boots: + +```bash +# Generate startup script for your platform +pm2 startup + +# Save current process list +pm2 save +``` + + ### Local Development Installation 1. **Clone the repository:** diff --git a/package.json b/package.json index 7299490..e778951 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "main": "server/index.js", "bin": { - "claude-code-ui": "server/index.js" + "claude-code-ui": "server/cli.js", + "cloudcli": "server/cli.js" }, "files": [ "server/", diff --git a/server/cli.js b/server/cli.js new file mode 100755 index 0000000..7401a9e --- /dev/null +++ b/server/cli.js @@ -0,0 +1,225 @@ +#!/usr/bin/env node +/** + * Claude Code UI CLI + * + * Provides command-line utilities for managing Claude Code UI + * + * Commands: + * (no args) - Start the server (default) + * start - Start the server + * status - Show configuration and data locations + * help - Show help information + * version - Show version information + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// ANSI color codes for terminal output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + + // Foreground colors + cyan: '\x1b[36m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + white: '\x1b[37m', + gray: '\x1b[90m', +}; + +// Helper to colorize text +const c = { + info: (text) => `${colors.cyan}${text}${colors.reset}`, + ok: (text) => `${colors.green}${text}${colors.reset}`, + warn: (text) => `${colors.yellow}${text}${colors.reset}`, + error: (text) => `${colors.yellow}${text}${colors.reset}`, + tip: (text) => `${colors.blue}${text}${colors.reset}`, + bright: (text) => `${colors.bright}${text}${colors.reset}`, + dim: (text) => `${colors.dim}${text}${colors.reset}`, +}; + +// Load package.json for version info +const packageJsonPath = path.join(__dirname, '../package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + +// Load environment variables from .env file if it exists +function loadEnvFile() { + try { + const envPath = path.join(__dirname, '../.env'); + const envFile = fs.readFileSync(envPath, 'utf8'); + envFile.split('\n').forEach(line => { + const trimmedLine = line.trim(); + if (trimmedLine && !trimmedLine.startsWith('#')) { + const [key, ...valueParts] = trimmedLine.split('='); + if (key && valueParts.length > 0 && !process.env[key]) { + process.env[key] = valueParts.join('=').trim(); + } + } + }); + } catch (e) { + // .env file is optional + } +} + +// Get the database path (same logic as db.js) +function getDatabasePath() { + loadEnvFile(); + return process.env.DATABASE_PATH || path.join(__dirname, 'database', 'auth.db'); +} + +// Get the installation directory +function getInstallDir() { + return path.join(__dirname, '..'); +} + +// Show status command +function showStatus() { + console.log(`\n${c.bright('Claude Code UI - Status')}\n`); + console.log(c.dim('═'.repeat(60))); + + // Version info + console.log(`\n${c.info('[INFO]')} Version: ${c.bright(packageJson.version)}`); + + // Installation location + const installDir = getInstallDir(); + console.log(`\n${c.info('[INFO]')} Installation Directory:`); + console.log(` ${c.dim(installDir)}`); + + // Database location + const dbPath = getDatabasePath(); + const dbExists = fs.existsSync(dbPath); + console.log(`\n${c.info('[INFO]')} Database Location:`); + console.log(` ${c.dim(dbPath)}`); + console.log(` Status: ${dbExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not created yet (will be created on first run)')}`); + + if (dbExists) { + const stats = fs.statSync(dbPath); + console.log(` Size: ${c.dim((stats.size / 1024).toFixed(2) + ' KB')}`); + console.log(` Modified: ${c.dim(stats.mtime.toLocaleString())}`); + } + + // Environment variables + console.log(`\n${c.info('[INFO]')} Configuration:`); + console.log(` PORT: ${c.bright(process.env.PORT || '3001')} ${c.dim(process.env.PORT ? '' : '(default)')}`); + console.log(` DATABASE_PATH: ${c.dim(process.env.DATABASE_PATH || '(using default location)')}`); + console.log(` CLAUDE_CLI_PATH: ${c.dim(process.env.CLAUDE_CLI_PATH || 'claude (default)')}`); + console.log(` CONTEXT_WINDOW: ${c.dim(process.env.CONTEXT_WINDOW || '160000 (default)')}`); + + // Claude projects folder + const claudeProjectsPath = path.join(process.env.HOME, '.claude', 'projects'); + const projectsExists = fs.existsSync(claudeProjectsPath); + console.log(`\n${c.info('[INFO]')} Claude Projects Folder:`); + console.log(` ${c.dim(claudeProjectsPath)}`); + console.log(` Status: ${projectsExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found')}`); + + // Config file location + const envFilePath = path.join(__dirname, '../.env'); + const envExists = fs.existsSync(envFilePath); + console.log(`\n${c.info('[INFO]')} Configuration File:`); + console.log(` ${c.dim(envFilePath)}`); + console.log(` Status: ${envExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found (using defaults)')}`); + + console.log('\n' + c.dim('═'.repeat(60))); + console.log(`\n${c.tip('[TIP]')} Hints:`); + console.log(` ${c.dim('>')} Set DATABASE_PATH env variable to use a custom database location`); + console.log(` ${c.dim('>')} Create .env file in installation directory for persistent config`); + console.log(` ${c.dim('>')} Run "claude-code-ui" or "cloudcli start" to start the server`); + console.log(` ${c.dim('>')} Access the UI at http://localhost:3001 (or custom PORT)\n`); +} + +// Show help +function showHelp() { + console.log(` +╔═══════════════════════════════════════════════════════════════╗ +║ Claude Code UI - Command Line Tool ║ +╚═══════════════════════════════════════════════════════════════╝ + +Usage: + claude-code-ui [command] + cloudcli [command] + +Commands: + start Start the Claude Code UI server (default) + status Show configuration and data locations + help Show this help information + version Show version information + +Examples: + $ claude-code-ui # Start the server + $ cloudcli status # Show configuration + $ cloudcli help # Show help + +Environment Variables: + PORT Set server port (default: 3001) + DATABASE_PATH Set custom database location + CLAUDE_CLI_PATH Set custom Claude CLI path + CONTEXT_WINDOW Set context window size (default: 160000) + +Configuration: + Create a .env file in the installation directory to set + persistent environment variables. Use 'cloudcli status' to + see the installation directory path. + +Documentation: + ${packageJson.homepage || 'https://github.com/siteboon/claudecodeui'} + +Report Issues: + ${packageJson.bugs?.url || 'https://github.com/siteboon/claudecodeui/issues'} +`); +} + +// Show version +function showVersion() { + console.log(`${packageJson.version}`); +} + +// Start the server +async function startServer() { + // Import and run the server + await import('./index.js'); +} + +// Main CLI handler +async function main() { + const args = process.argv.slice(2); + const command = args[0] || 'start'; + + switch (command) { + case 'start': + await startServer(); + break; + case 'status': + case 'info': + showStatus(); + break; + case 'help': + case '-h': + case '--help': + showHelp(); + break; + case 'version': + case '-v': + case '--version': + showVersion(); + break; + default: + console.error(`\n❌ Unknown command: ${command}`); + console.log(' Run "cloudcli help" for usage information.\n'); + process.exit(1); + } +} + +// Run the CLI +main().catch(error => { + console.error('\n❌ Error:', error.message); + process.exit(1); +}); diff --git a/server/database/db.js b/server/database/db.js index b6959e5..b8c56a2 100644 --- a/server/database/db.js +++ b/server/database/db.js @@ -8,6 +8,20 @@ import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// ANSI color codes for terminal output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + cyan: '\x1b[36m', + dim: '\x1b[2m', +}; + +const c = { + info: (text) => `${colors.cyan}${text}${colors.reset}`, + bright: (text) => `${colors.bright}${text}${colors.reset}`, + dim: (text) => `${colors.dim}${text}${colors.reset}`, +}; + // Use DATABASE_PATH environment variable if set, otherwise use default location const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db'); const INIT_SQL_PATH = path.join(__dirname, 'init.sql'); @@ -28,7 +42,18 @@ if (process.env.DATABASE_PATH) { // Create database connection const db = new Database(DB_PATH); -console.log(`Connected to SQLite database at: ${DB_PATH}`); + +// Show app installation path prominently +const appInstallPath = path.join(__dirname, '../..'); +console.log(''); +console.log(c.dim('═'.repeat(60))); +console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`); +console.log(`${c.info('[INFO]')} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`); +if (process.env.DATABASE_PATH) { + console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`); +} +console.log(c.dim('═'.repeat(60))); +console.log(''); // Initialize database with schema const initializeDatabase = async () => { diff --git a/server/index.js b/server/index.js index 9a39a5d..4562f80 100755 --- a/server/index.js +++ b/server/index.js @@ -8,6 +8,26 @@ import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// ANSI color codes for terminal output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + cyan: '\x1b[36m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + dim: '\x1b[2m', +}; + +const c = { + info: (text) => `${colors.cyan}${text}${colors.reset}`, + ok: (text) => `${colors.green}${text}${colors.reset}`, + warn: (text) => `${colors.yellow}${text}${colors.reset}`, + tip: (text) => `${colors.blue}${text}${colors.reset}`, + bright: (text) => `${colors.bright}${text}${colors.reset}`, + dim: (text) => `${colors.dim}${text}${colors.reset}`, +}; + try { const envPath = path.join(__dirname, '../.env'); const envFile = fs.readFileSync(envPath, 'utf8'); @@ -116,7 +136,7 @@ async function setupProjectsWatcher() { }); } catch (error) { - console.error('❌ Error handling project changes:', error); + console.error('[ERROR] Error handling project changes:', error); } }, 300); // 300ms debounce (slightly faster than before) }; @@ -129,13 +149,13 @@ async function setupProjectsWatcher() { .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath)) .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath)) .on('error', (error) => { - console.error('❌ Chokidar watcher error:', error); + console.error('[ERROR] Chokidar watcher error:', error); }) .on('ready', () => { }); } catch (error) { - console.error('❌ Failed to setup projects watcher:', error); + console.error('[ERROR] Failed to setup projects watcher:', error); } } @@ -157,13 +177,13 @@ const wss = new WebSocketServer({ // Verify token const user = authenticateWebSocket(token); if (!user) { - console.log('❌ WebSocket authentication failed'); + console.log('[WARN] WebSocket authentication failed'); return false; } // Store user info in the request for later use info.req.user = user; - console.log('✅ WebSocket authenticated for user:', user.username); + console.log('[OK] WebSocket authenticated for user:', user.username); return true; } }); @@ -462,7 +482,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) = const { projectName } = req.params; const { filePath } = req.query; - console.log('📄 File read request:', projectName, filePath); + console.log('[DEBUG] File read request:', projectName, filePath); // Security: ensure the requested path is inside the project root if (!filePath) { @@ -503,7 +523,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re const { projectName } = req.params; const { path: filePath } = req.query; - console.log('🖼️ Binary file serve request:', projectName, filePath); + console.log('[DEBUG] Binary file serve request:', projectName, filePath); // Security: ensure the requested path is inside the project root if (!filePath) { @@ -557,7 +577,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) = const { projectName } = req.params; const { filePath, content } = req.body; - console.log('💾 File save request:', projectName, filePath); + console.log('[DEBUG] File save request:', projectName, filePath); // Security: ensure the requested path is inside the project root if (!filePath) { @@ -628,7 +648,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) const hiddenFiles = files.filter(f => f.name.startsWith('.')); res.json(files); } catch (error) { - console.error('❌ File tree error:', error.message); + console.error('[ERROR] File tree error:', error.message); res.status(500).json({ error: error.message }); } }); @@ -636,7 +656,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) // WebSocket connection handler that routes based on URL path wss.on('connection', (ws, request) => { const url = request.url; - console.log('🔗 Client connected to:', url); + console.log('[INFO] Client connected to:', url); // Parse URL to get pathname without query parameters const urlObj = new URL(url, 'http://localhost'); @@ -647,14 +667,14 @@ wss.on('connection', (ws, request) => { } else if (pathname === '/ws') { handleChatConnection(ws); } else { - console.log('❌ Unknown WebSocket path:', pathname); + console.log('[WARN] Unknown WebSocket path:', pathname); ws.close(); } }); // Handle chat WebSocket connections function handleChatConnection(ws) { - console.log('💬 Chat WebSocket connected'); + console.log('[INFO] Chat WebSocket connected'); // Add to connected clients for project updates connectedClients.add(ws); @@ -664,28 +684,28 @@ function handleChatConnection(ws) { const data = JSON.parse(message); if (data.type === 'claude-command') { - console.log('💬 User message:', data.command || '[Continue/Resume]'); + console.log('[DEBUG] User message:', data.command || '[Continue/Resume]'); console.log('📁 Project:', data.options?.projectPath || 'Unknown'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); // Use Claude Agents SDK await queryClaudeSDK(data.command, data.options, ws); } else if (data.type === 'cursor-command') { - console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]'); + console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]'); console.log('📁 Project:', data.options?.cwd || 'Unknown'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); console.log('🤖 Model:', data.options?.model || 'default'); await spawnCursor(data.command, data.options, ws); } else if (data.type === 'cursor-resume') { // Backward compatibility: treat as cursor-command with resume and no prompt - console.log('🖱️ Cursor resume session (compat):', data.sessionId); + console.log('[DEBUG] Cursor resume session (compat):', data.sessionId); await spawnCursor('', { sessionId: data.sessionId, resume: true, cwd: data.options?.cwd }, ws); } else if (data.type === 'abort-session') { - console.log('🛑 Abort session request:', data.sessionId); + console.log('[DEBUG] Abort session request:', data.sessionId); const provider = data.provider || 'claude'; let success; @@ -703,7 +723,7 @@ function handleChatConnection(ws) { success })); } else if (data.type === 'cursor-abort') { - console.log('🛑 Abort Cursor session:', data.sessionId); + console.log('[DEBUG] Abort Cursor session:', data.sessionId); const success = abortCursorSession(data.sessionId); ws.send(JSON.stringify({ type: 'session-aborted', @@ -742,7 +762,7 @@ function handleChatConnection(ws) { })); } } catch (error) { - console.error('❌ Chat WebSocket error:', error.message); + console.error('[ERROR] Chat WebSocket error:', error.message); ws.send(JSON.stringify({ type: 'error', error: error.message @@ -776,7 +796,7 @@ function handleShellConnection(ws) { const initialCommand = data.initialCommand; const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell'; - console.log('🚀 Starting shell in:', projectPath); + console.log('[INFO] Starting shell in:', projectPath); console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session')); console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider); if (initialCommand) { @@ -889,7 +909,7 @@ function handleShellConnection(ws) { let match; while ((match = pattern.exec(data)) !== null) { const url = match[1]; - console.log('🔗 Detected URL for opening:', url); + console.log('[DEBUG] Detected URL for opening:', url); // Send URL opening message to client ws.send(JSON.stringify({ @@ -899,7 +919,7 @@ function handleShellConnection(ws) { // Replace the OPEN_URL pattern with a user-friendly message if (pattern.source.includes('OPEN_URL')) { - outputData = outputData.replace(match[0], `🌐 Opening in browser: ${url}`); + outputData = outputData.replace(match[0], `[INFO] Opening in browser: ${url}`); } } }); @@ -925,7 +945,7 @@ function handleShellConnection(ws) { }); } catch (spawnError) { - console.error('❌ Error spawning process:', spawnError); + console.error('[ERROR] Error spawning process:', spawnError); ws.send(JSON.stringify({ type: 'output', data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n` @@ -951,7 +971,7 @@ function handleShellConnection(ws) { } } } catch (error) { - console.error('❌ Shell WebSocket error:', error.message); + console.error('[ERROR] Shell WebSocket error:', error.message); if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'output', @@ -970,7 +990,7 @@ function handleShellConnection(ws) { }); ws.on('error', (error) => { - console.error('❌ Shell WebSocket error:', error); + console.error('[ERROR] Shell WebSocket error:', error); }); } // Audio transcription endpoint @@ -1411,28 +1431,37 @@ async function startServer() { try { // Initialize authentication database await initializeDatabase(); - console.log('✅ Database initialization skipped (testing)'); // Check if running in production mode (dist folder exists) const distIndexPath = path.join(__dirname, '../dist/index.html'); const isProduction = fs.existsSync(distIndexPath); // Log Claude implementation mode - console.log('🚀 Using Claude Agents SDK for Claude integration'); - console.log(`📦 Running in ${isProduction ? 'PRODUCTION' : 'DEVELOPMENT'} mode`); + console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`); + console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`); if (!isProduction) { - console.log(`⚠️ Note: Requests will be proxied to Vite dev server at http://localhost:${process.env.VITE_PORT || 5173}`); + console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`); } server.listen(PORT, '0.0.0.0', async () => { - console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`); + const appInstallPath = path.join(__dirname, '..'); + + console.log(''); + console.log(c.dim('═'.repeat(63))); + console.log(` ${c.bright('Claude Code UI Server - Ready')}`); + console.log(c.dim('═'.repeat(63))); + console.log(''); + console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://0.0.0.0:' + PORT)}`); + console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`); + console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`); + console.log(''); // Start watching the projects folder for changes await setupProjectsWatcher(); }); } catch (error) { - console.error('❌ Failed to start server:', error); + console.error('[ERROR] Failed to start server:', error); process.exit(1); } }