UNPKG

@hhoangphuoc/escape-room-cli

Version:

A CLI for playing AI-generated escape room games. Install globally with: npm install -g @hhoangphuoc/escape-room-cli

833 lines (832 loc) • 32.5 kB
import { getApiUrl } from './apiConfig.js'; import { validateOpenAIApiKey, validateAnthropicApiKey } from './validation.js'; import fs from 'fs'; import path from 'path'; import os from 'os'; const USER_CONFIG_FILE = path.join(os.homedir(), '.escape-room-config.json'); // Help Command Handler export const handleHelpCommand = (userContext, gameContext) => { const baseCommands = [ { command: '/help', description: 'Show available commands and their usage' }, { command: '/instructions', description: 'Show detailed game instructions', }, { command: '/history', description: 'Show command history' }, { command: '/cost', description: 'Show current session cost summary' }, { command: '/usage', description: 'Show detailed AI usage statistics' }, ]; const authCommands = userContext.sessionToken ? [ { command: '/newgame', description: 'Start a new AI-generated game', usage: '/newgame', }, { command: '/hint', description: 'Get a hint for the password of this room' }, { command: '/look', description: 'Look around the current room' }, { command: '/inspect [object]', description: 'Inspect an object in the room', usage: '/inspect table', }, { command: '/guess [object] [answer]', description: 'Guess the puzzle answer for an object', usage: '/guess safe 1234', }, { command: '/password [password]', description: 'Submit the password to unlock the door', usage: '/password escape123', }, { command: '/leaderboard', description: 'View the top 10 players on the leaderboard', }, { command: '/login', description: 'Login to the game' }, { command: '/logout', description: 'End your current session' }, ] : [ { command: '/login', description: 'Login to the game' }, { command: '/register', description: 'Start the registration process' }, ]; const aiCommands = userContext.hasAICapability ? [{ command: '/model', description: 'Change AI model for chat assistance' }] : []; const commands = [...baseCommands, ...authCommands, ...aiCommands]; return { success: true, message: 'Available commands:', commands, currentContext: { hasAI: userContext.hasAICapability, currentModel: userContext.selectedModel?.label, currentRoom: gameContext.currentRoomName !== 'Loading...' ? gameContext.currentRoomName : undefined, gameMode: gameContext.currentGameMode !== 'unknown' ? gameContext.currentGameMode : undefined, isAuthenticated: !!userContext.sessionToken, }, }; }; // Look Command Handler export const handleLookCommand = async (userContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You are not in a game session. Please login first.', error: 'Not authenticated', roomData: { name: '', objects: [] }, }; } try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/game/look`, { headers: { Authorization: `Bearer ${userContext.sessionToken}` }, }); if (response.ok) { const data = (await response.json()); return { success: true, message: data.message || `You are in ${data.roomName}`, roomData: { name: data.roomName || 'Unknown Room', background: data.background, objects: data.objects || [], }, }; } else { const errorData = (await response.json()); return { success: false, message: 'Failed to look around the room', error: errorData.error || 'Unknown error', roomData: { name: '', objects: [] }, }; } } catch (error) { return { success: false, message: 'Network error occurred while looking around', error: 'Network error', roomData: { name: '', objects: [] }, }; } }; // Inspect Command Handler export const handleInspectCommand = async (objectName, userContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You are not in a game session. Please login first.', error: 'Not authenticated', }; } if (!objectName) { return { success: false, message: 'Object name is required.', error: 'Missing object name', }; } try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/game/inspect?object=${encodeURIComponent(objectName)}`, { headers: { Authorization: `Bearer ${userContext.sessionToken}` }, }); const data = (await response.json()); if (response.ok && data.object) { return { success: true, message: data.message || `Inspecting ${objectName}...`, objectData: { name: data.object.name, description: data.object.description, puzzle: data.object.puzzle, answer: data.object.answer, unlocked: data.object.unlocked || false, details: data.object.details, }, }; } else { return { success: false, message: data.error || `Could not inspect ${objectName}.`, error: data.error || 'Object not found', }; } } catch (error) { return { success: false, message: 'Network error occurred while inspecting object', error: 'Network error', }; } }; // Guess Command Handler export const handleGuessCommand = async (objectName, answer, userContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You are not in a game session. Please login first.', error: 'Not authenticated', objectData: { name: objectName, unlocked: false, correctAnswer: false }, }; } if (!objectName || !answer) { return { success: false, message: 'Both object name and answer are required.', error: 'Missing parameters', objectData: { name: objectName, unlocked: false, correctAnswer: false }, }; } try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/game/guess?object=${encodeURIComponent(objectName)}&answer=${encodeURIComponent(answer)}`, { method: 'POST', headers: { Authorization: `Bearer ${userContext.sessionToken}`, 'Content-Type': 'application/json', }, }); const data = (await response.json()); if (response.ok && data.object) { return { success: true, message: data.message, objectData: { name: data.object.name, unlocked: data.object.unlocked || false, correctAnswer: data.object.unlocked || false, }, }; } else { return { success: false, message: data.error || 'Failed to process guess.', error: data.error || 'Guess failed', objectData: { name: objectName, unlocked: false, correctAnswer: false }, }; } } catch (error) { return { success: false, message: 'Network error occurred while processing guess', error: 'Network error', objectData: { name: objectName, unlocked: false, correctAnswer: false }, }; } }; // Password Command Handler export const handlePasswordCommand = async (password, userContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You are not in a game session. Please login first.', error: 'Not authenticated', gameResult: { escaped: false, gameCompleted: false }, }; } if (!password) { return { success: false, message: 'Password is required.', error: 'Missing password', gameResult: { escaped: false, gameCompleted: false }, }; } try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/game/password?password=${encodeURIComponent(password)}`, { method: 'POST', headers: { Authorization: `Bearer ${userContext.sessionToken}`, 'Content-Type': 'application/json', }, }); const data = (await response.json()); return { success: data.escaped || false, message: data.message + (data.timeElapsed ? `\nTime: ${data.timeElapsed} seconds` : ''), gameResult: { escaped: data.escaped || false, gameCompleted: data.gameCompleted || false, timeElapsed: data.timeElapsed, hintsUsed: data.hintsUsed, }, }; } catch (error) { return { success: false, message: 'Network error occurred while checking password', error: 'Network error', gameResult: { escaped: false, gameCompleted: false }, }; } }; // Hint Command Handler export const handleHintCommand = async (userContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You are not in a game session. Please login first.', error: 'Not authenticated', hintData: { hint: '', hintsUsed: 0 }, }; } try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/game/hint`, { headers: { Authorization: `Bearer ${userContext.sessionToken}` }, }); const data = (await response.json()); let hintText = ''; if (typeof data.hint === 'string') { hintText = data.hint; } else if (data.hint) { hintText = JSON.stringify(data.hint, null, 2); } else if (data.message) { hintText = typeof data.message === 'string' ? data.message : JSON.stringify(data.message, null, 2); } else { hintText = 'No hints available.'; } return { success: true, message: hintText, hintData: { hint: hintText, hintsUsed: data.hintsUsed || 0, hintType: 'general', }, }; } catch (error) { return { success: false, message: 'Network error occurred while getting hint', error: 'Network error', hintData: { hint: '', hintsUsed: 0 }, }; } }; // New Game Command Handler export const handleNewGameCommand = async (mode = 'single-room', userContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You are not in a game session. Please login first.', error: 'Not authenticated', gameData: { id: '', name: '', background: '', mode: '', currentRoom: 1, totalRooms: 1, objectCount: 0, }, }; } const requestedMode = mode === 'multi-room' ? 'multi-room' : 'single-room'; try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/game/newgame`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userContext.sessionToken}`, }, body: JSON.stringify({ mode: requestedMode }), }); const data = (await response.json()); if (data.success && data.game) { const gameInfo = data.game; const gameMessage = ` New ${gameInfo.mode || 'custom'} game created successfully! Room ${gameInfo.currentRoom || 1}${gameInfo.totalRooms && gameInfo.totalRooms > 1 ? ` of ${gameInfo.totalRooms}` : ''}: ${gameInfo.name} ${gameInfo.background || ''} This room contains ${gameInfo.objectCount !== undefined ? gameInfo.objectCount : '?'} objects. Use /look to see them. ${data.game.roomData?.hint ? `\nHint: ${data.game.roomData.hint}` : ''} Password needed to escape. Use /password [your_guess] when ready!`; return { success: true, message: gameMessage.trim(), gameData: { id: gameInfo.id || '', name: gameInfo.name || 'Untitled Room', background: gameInfo.background || 'No description provided.', mode: gameInfo.mode || requestedMode, currentRoom: gameInfo.currentRoom || 1, totalRooms: gameInfo.totalRooms || 1, objectCount: gameInfo.objectCount || 0, startTime: gameInfo.startTime, }, }; } else { return { success: false, message: `Failed to create new game: ${data.error || 'Unknown error'}`, error: data.error || 'Unknown error', gameData: { id: '', name: '', background: '', mode: '', currentRoom: 1, totalRooms: 1, objectCount: 0, }, }; } } catch (error) { return { success: false, message: 'Network error occurred while creating new game', error: 'Network error', gameData: { id: '', name: '', background: '', mode: '', currentRoom: 1, totalRooms: 1, objectCount: 0, }, }; } }; // Enhanced Leaderboard Command Handler with cost support export const handleLeaderboardCommand = async (userContext, viewMode = 'time') => { if (!userContext.sessionToken) { return { success: false, message: 'You are not in a game session. Please login first.', error: 'Not authenticated', leaderboardData: { entries: [], count: 0, mode: 'all' }, }; } try { const apiUrl = getApiUrl(); // Determine which endpoint to use based on view mode let endpoint = '/api/game/leaderboard/games'; if (viewMode === 'cost' || viewMode === 'efficiency') { endpoint = '/api/game/leaderboard/cost'; } const response = await fetch(`${apiUrl}${endpoint}`, { headers: { Authorization: `Bearer ${userContext.sessionToken}` }, }); if (response.ok) { const data = (await response.json()); if (data.leaderboard && data.leaderboard.length > 0) { const entries = data.leaderboard.map((entry, index) => ({ rank: index + 1, userName: entry.userName, timeElapsed: entry.timeElapsed, hintsUsed: entry.hintsUsed, gameMode: entry.gameMode, completedAt: entry.completedAt, totalCost: entry.totalCost, costEfficiency: entry.costEfficiency, aiRequestsUsed: entry.aiRequestsUsed, })); // Build leaderboard text based on view mode let leaderboardText = ''; if (viewMode === 'cost') { leaderboardText = 'šŸ’° COST-BASED LEADERBOARD šŸ’°\n\n'; leaderboardText += 'Rank | Player | Time | Cost | Efficiency\n'; leaderboardText += '─────┼──────────┼───────┼──────────┼───────────\n'; entries.forEach((entry) => { const rank = entry.rank.toString().padStart(4, ' '); const player = entry.userName.substring(0, 9).padEnd(9, ' '); const time = `${entry.timeElapsed}s`.padEnd(6, ' '); const cost = entry.totalCost ? `$${entry.totalCost.toFixed(3)}`.padEnd(8, ' ') : 'N/A'.padEnd(8, ' '); const efficiency = entry.costEfficiency ? `${entry.costEfficiency.toFixed(0)}%` : 'N/A'; leaderboardText += `${rank} │ ${player} │ ${time} │ ${cost} │ ${efficiency}\n`; }); } else if (viewMode === 'efficiency') { leaderboardText = '⚔ EFFICIENCY LEADERBOARD ⚔\n\n'; leaderboardText += 'Rank | Player | Efficiency | Cost | Time\n'; leaderboardText += '─────┼──────────┼───────────┼──────────┼──────\n'; // Sort by efficiency for this view const sortedByEfficiency = [...entries].sort((a, b) => (b.costEfficiency || 0) - (a.costEfficiency || 0)); sortedByEfficiency.forEach((entry, index) => { const rank = (index + 1).toString().padStart(4, ' '); const player = entry.userName.substring(0, 9).padEnd(9, ' '); const efficiency = entry.costEfficiency ? `${entry.costEfficiency.toFixed(0)}%`.padEnd(10, ' ') : 'N/A'.padEnd(10, ' '); const cost = entry.totalCost ? `$${entry.totalCost.toFixed(3)}`.padEnd(8, ' ') : 'N/A'.padEnd(8, ' '); const time = `${entry.timeElapsed}s`; leaderboardText += `${rank} │ ${player} │ ${efficiency} │ ${cost} │ ${time}\n`; }); } else { // Default time-based view leaderboardText = 'šŸ† TIME-BASED LEADERBOARD šŸ†\n\n'; leaderboardText += 'Rank | Player | Time | Hints | Mode\n'; leaderboardText += '─────┼────────┼──────┼───────┼─────────\n'; entries.forEach((entry) => { const rank = entry.rank.toString().padStart(4, ' '); const player = entry.userName.substring(0, 8).padEnd(8, ' '); const time = `${entry.timeElapsed}s`.padEnd(6, ' '); const hints = entry.hintsUsed.toString().padStart(5, ' '); const mode = entry.gameMode.substring(0, 9).padEnd(9, ' '); leaderboardText += `${rank} │ ${player} │ ${time} │${hints} │ ${mode}\n`; }); } leaderboardText += '\nšŸ’” Switch views: /leaderboard [time|cost|efficiency]'; return { success: true, message: leaderboardText, leaderboardData: { entries, count: entries.length, mode: viewMode, }, }; } else { return { success: true, message: 'No completed games found on the leaderboard yet. Be the first to complete a game!', leaderboardData: { entries: [], count: 0, mode: viewMode }, }; } } else { const errorData = (await response.json()); return { success: false, message: 'Failed to fetch leaderboard', error: errorData.error || 'Unknown error', leaderboardData: { entries: [], count: 0, mode: viewMode }, }; } } catch (error) { return { success: false, message: 'Network error occurred while fetching leaderboard', error: 'Network error', leaderboardData: { entries: [], count: 0, mode: viewMode }, }; } }; // Logout Command Handler export const handleLogoutCommand = () => { return { success: true, message: 'Logged out successfully. Type /login to login again.', }; }; // Login Command Handler export const handleLoginCommand = async () => { let configUserId; let configApiKey; let configName; let configProvider = 'openai'; if (fs.existsSync(USER_CONFIG_FILE)) { try { const config = JSON.parse(fs.readFileSync(USER_CONFIG_FILE, 'utf-8')); configUserId = config.userId; configName = config.name; // Validate OpenAI API key if (config.apiKeys?.openai) { configApiKey = config.apiKeys.openai; configProvider = 'openai'; // Validate the stored OpenAI API key if (configApiKey) { try { const validation = await validateOpenAIApiKey(configApiKey); if (!validation.isValid) { return { success: false, message: `Stored OpenAI API key is invalid: ${validation.error}`, error: 'Invalid API key', }; } } catch (validationError) { // If validation fails, warn but continue (maybe backend is unavailable) console.warn('API key validation failed during login:', validationError); } } // Validate Anthropic API key (NOT IMPLEMENTED) } else if (config.apiKeys?.anthropic) { configApiKey = config.apiKeys.anthropic; configProvider = 'anthropic'; if (configApiKey) { const validation = validateAnthropicApiKey(configApiKey); if (!validation.isValid) { return { success: false, message: `Stored Anthropic API key is invalid: ${validation.error}`, error: 'Invalid API key', }; } } } } catch (error) { return { success: false, message: 'Error reading config file', error: 'Config file corrupted', }; } } if (configUserId) { try { // Import dynamically to avoid circular dependencies const { handleLogin } = await import('../components/UserRegistration.js'); const loginResponse = await handleLogin(configUserId, configApiKey, configProvider); const loginData = (await loginResponse.json()); if (loginResponse.ok && loginData.token) { return { success: true, message: 'Logged in successfully.', userData: { userId: configUserId, userName: configName || 'Unknown User', sessionToken: loginData.token, apiKey: configApiKey, apiKeyProvider: configProvider, }, }; } else { return { success: false, message: `Login failed: ${loginData.error || 'Unknown error'}`, error: loginData.error || 'Unknown error', }; } } catch (error) { return { success: false, message: 'Network error during login', error: 'Network error', }; } } else { return { success: false, message: 'No user configuration found. Please register first.', error: 'No config found', }; } }; // Natural Language Command Handler export const handleNaturalLanguageCommand = async (text, userContext, model) => { if (!userContext.sessionToken) { return { success: false, message: 'Please login or register to use AI chat.', }; } const apiKey = userContext.cliApiKey || process.env['OPENAI_API_KEY'] || process.env['ANTHROPIC_API_KEY']; if (!apiKey) { return { success: false, message: 'Error: No API key available for AI chat.', }; } // Simplified API key validation - just check basic format try { let isValidFormat = false; // Check for OpenAI key format (sk-proj- or sk-) if (apiKey.startsWith('sk-proj-') || (apiKey.startsWith('sk-') && apiKey.length >= 20)) { isValidFormat = true; } // Check for Anthropic key format else if (apiKey.startsWith('sk-ant-') || apiKey.startsWith('anthropic')) { isValidFormat = true; } if (!isValidFormat) { return { success: false, message: 'Error: Invalid API key format. Please check your API key.', }; } } catch (validationError) { // If validation fails, warn but continue (maybe backend is unavailable) console.warn('API key validation failed during natural language command:', validationError); } try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/game/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userContext.sessionToken}`, }, body: JSON.stringify({ message: text, model: model.value, }), }); if (!response.ok) { const errorData = (await response.json()); return { success: false, message: `AI Chat Error: ${errorData.error || 'Unknown error'}`, }; } const data = (await response.json()); return { success: true, message: data.response || "AI couldn't understand that.", }; } catch (error) { return { success: false, message: 'Error with AI chat request.' }; } }; // Instructions Command Handler export const handleInstructionsCommand = (_userContext, _gameContext) => { return { success: true, message: 'Showing game instructions...', showInstructions: true, }; }; // Usage Command Handler export const handleUsageCommand = async (userContext, gameContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You must be logged in to view usage statistics.', }; } try { const apiUrl = getApiUrl(); const gameIdParam = gameContext.currentGameId ? `?gameId=${gameContext.currentGameId}` : ''; // Get monitoring data const response = await fetch(`${apiUrl}/api/usage/monitoring${gameIdParam}`, { headers: { Authorization: `Bearer ${userContext.sessionToken}` }, }); if (!response.ok) { return { success: false, message: 'Failed to fetch usage data. Please try again later.', }; } const data = await response.json(); if (!data.success) { return { success: false, message: data.error || 'Failed to retrieve usage data.', }; } const usage = data.data; // Transform API response to CostDashboard expected format const usageData = { currentSessionCost: usage.currentSessionCost || 0, currentSessionTokens: usage.currentSessionTokens || 0, userTotalCost: usage.userTotalCost || 0, userTotalTokens: usage.userTotalTokens || 0, lastRequestCost: usage.lastRequestCost || 0, lastRequestTokens: usage.lastRequestTokens || 0, averageCostPerRequest: usage.averageCostPerRequest || 0, sessionStartTime: usage.sessionStartTime, modelUsageBreakdown: usage.modelUsageBreakdown || {}, budgetSettings: usage.budgetSettings, alerts: usage.alerts || [] }; return { success: true, message: 'Opening usage dashboard...', showUsageDashboard: true, usageData: usageData, }; } catch (error) { return { success: false, message: 'āŒ Error fetching usage statistics. Please check your connection and try again.', }; } }; // Enhanced Cost Command Handler with detailed analytics export const handleCostCommand = async (userContext, gameContext) => { if (!userContext.sessionToken) { return { success: false, message: 'You must be logged in to view cost information.', }; } try { const apiUrl = getApiUrl(); const gameIdParam = gameContext.currentGameId ? `?gameId=${gameContext.currentGameId}` : ''; const response = await fetch(`${apiUrl}/api/usage/monitoring${gameIdParam}`, { headers: { Authorization: `Bearer ${userContext.sessionToken}` }, }); if (!response.ok) { return { success: false, message: 'āŒ Unable to fetch cost data.', }; } const data = await response.json(); if (!data.success) { return { success: false, message: 'āŒ Failed to retrieve cost data.', }; } const cost = data.data; // Transform API response to CostMonitor expected format const costData = { currentSessionCost: cost.currentSessionCost || 0, currentSessionTokens: cost.currentSessionTokens || 0, userTotalCost: cost.userTotalCost || 0, userTotalTokens: cost.userTotalTokens || 0, lastRequestCost: cost.lastRequestCost || 0, lastRequestTokens: cost.lastRequestTokens || 0, averageCostPerRequest: cost.averageCostPerRequest || 0, modelUsageBreakdown: cost.modelUsageBreakdown || {} }; return { success: true, message: 'Opening cost monitor...', showCostMonitor: true, costData: costData, }; } catch (error) { return { success: false, message: 'āŒ Error fetching cost data. Please try again or check your connection.', }; } }; // Removed: handlePricingCommand - simplified cost monitoring // Removed: handleCompareCommand - simplified cost monitoring // Removed: handleEstimateCommand - simplified cost monitoring