UNPKG

qnce-engine

Version:

Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization

405 lines 16.3 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.playInteractive = main; const fs_1 = require("fs"); const readline_1 = require("readline"); const core_js_1 = require("../engine/core.js"); const core_js_2 = require("../telemetry/core.js"); const StorageAdapters_js_1 = require("../persistence/StorageAdapters.js"); const demo_story_js_1 = require("../engine/demo-story.js"); function displayNode(session) { const { engine } = session; const currentNode = engine.getCurrentNode(); const choices = engine.getAvailableChoices(); console.log('\n' + '='.repeat(60)); console.log(currentNode.text); console.log('='.repeat(60)); if (choices.length > 0) { console.log('\nChoices:'); choices.forEach((choice, index) => { console.log(`${index + 1}. ${choice.text}`); }); } else { console.log('\n[Story Complete]'); } // Show undo/redo status const undoCount = engine.getUndoCount(); const redoCount = engine.getRedoCount(); console.log(`\nUndo: ${undoCount} available | Redo: ${redoCount} available`); } function displayHelp() { console.log('\nCommands:'); console.log(' 1-9 : Select choice by number'); console.log(' u, undo : Undo last action'); console.log(' r, redo : Redo last undone action'); console.log(' h, help : Show this help'); console.log(' s, save : Save current state'); console.log(' l, load : Load saved state'); console.log(' f, flags: Show current flags'); console.log(' hist : Show history summary'); console.log(' q, quit : Exit the session'); } function displayFlags(session) { const { engine } = session; const flags = engine.getState().flags; console.log('\n--- Current Flags ---'); if (Object.keys(flags).length === 0) { console.log('No flags set'); } else { Object.entries(flags).forEach(([key, value]) => { console.log(`${key}: ${JSON.stringify(value)}`); }); } } function displayHistory(session) { const { engine } = session; const summary = engine.getHistorySummary(); console.log('\n--- History Summary ---'); console.log(`Undo entries: ${summary.undoEntries.length}`); console.log(`Redo entries: ${summary.redoEntries.length}`); if (summary.undoEntries.length > 0) { console.log('\nRecent undo history:'); summary.undoEntries.slice(-5).forEach((entry, index) => { console.log(` ${index + 1}. ${entry.action || 'Action'} (${entry.timestamp})`); }); } } async function saveState(session) { const { engine, rl } = session; return new Promise((resolve) => { rl.question('Enter filename to save (or press Enter for default): ', async (filename) => { const saveFile = filename || 'qnce-save.json'; try { const serializedState = await engine.saveState({ includeFlowEvents: true }); require('fs').writeFileSync(saveFile, JSON.stringify(serializedState, null, 2)); console.log(`✅ State saved to ${saveFile}`); } catch (error) { console.error('❌ Failed to save state:', error?.message || error); } resolve(); }); }); } async function loadState(session) { const { engine, rl } = session; return new Promise((resolve) => { rl.question('Enter filename to load (or press Enter for default): ', async (filename) => { const loadFile = filename || 'qnce-save.json'; try { const data = (0, fs_1.readFileSync)(loadFile, 'utf-8'); const serializedState = JSON.parse(data); await engine.loadState(serializedState); console.log(`✅ State loaded from ${loadFile}`); displayNode(session); } catch (error) { console.error('❌ Failed to load state:', error?.message || error); } resolve(); }); }); } function handleCommand(session, input) { const { engine } = session; const choices = engine.getAvailableChoices(); return new Promise((resolve) => { const command = input.trim().toLowerCase(); // Handle numeric choices const choiceNumber = parseInt(command); if (!isNaN(choiceNumber) && choiceNumber >= 1 && choiceNumber <= choices.length) { const selectedChoice = choices[choiceNumber - 1]; engine.selectChoice(selectedChoice); displayNode(session); resolve(false); return; } // Handle text commands switch (command) { case 'u': case 'undo': const undoResult = engine.undo(); if (undoResult.success) { console.log(`✅ Undid: ${undoResult.entry?.action || 'action'}`); displayNode(session); } else { console.log('❌ Nothing to undo'); } resolve(false); break; case 'r': case 'redo': const redoResult = engine.redo(); if (redoResult.success) { console.log(`✅ Redid: ${redoResult.entry?.action || 'action'}`); displayNode(session); } else { console.log('❌ Nothing to redo'); } resolve(false); break; case 'h': case 'help': displayHelp(); resolve(false); break; case 'f': case 'flags': displayFlags(session); resolve(false); break; case 'hist': displayHistory(session); resolve(false); break; case 's': case 'save': saveState(session).then(() => resolve(false)); break; case 'l': case 'load': loadState(session).then(() => resolve(false)); break; case 'q': case 'quit': console.log('👋 Thanks for playing!'); resolve(true); break; default: console.log('❓ Unknown command. Type "help" for available commands.'); resolve(false); break; } }); } async function startInteractiveSession(session) { const { rl } = session; console.log('🎮 QNCE Interactive Session Started'); console.log('Type "help" for available commands'); // Configure undo/redo and autosave session.engine.configureUndoRedo({ enabled: true, maxUndoEntries: 100, maxRedoEntries: 50 }); session.engine.configureAutosave({ enabled: true, triggers: ['choice', 'flag-change'], throttleMs: 100, maxEntries: 20, includeMetadata: true }); displayNode(session); const promptUser = async () => { return new Promise((resolve) => { rl.question('\n> ', async (input) => { const shouldQuit = await handleCommand(session, input); if (shouldQuit) { rl.close(); resolve(); } else { await promptUser(); resolve(); } }); }); }; await promptUser(); } function main() { const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log('QNCE Interactive CLI'); console.log('Usage: qnce-play [story-file.json] [--storage <type>] [--storage-prefix <p>] [--storage-dir <dir>] [--storage-db <name>] [--non-interactive] [--save-key <key>] [--load-key <key>] [--telemetry <console|file|none>] [--telemetry-file <path>] [--telemetry-sample <0..1>] [--telemetry-report]'); console.log(''); console.log('If no story file is provided, the demo story will be used.'); console.log(''); console.log('Commands during play:'); displayHelp(); return; } let storyData; // Parse args to find a positional story file (skip option values) const optsWithValue = new Set([ '--storage', '--storage-prefix', '--storage-dir', '--storage-db', '--save-key', '--load-key', // telemetry flags with values '--telemetry', '--telemetry-file', '--telemetry-sample' ]); let storyFile; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a.startsWith('-')) { if (optsWithValue.has(a)) i++; // skip its value continue; } // first non-option token is treated as story file storyFile = a; break; } if (storyFile) { try { console.log(`📖 Loading story: ${storyFile}`); const jsonData = JSON.parse((0, fs_1.readFileSync)(storyFile, 'utf-8')); storyData = (0, core_js_1.loadStoryData)(jsonData); } catch (error) { console.error(`❌ Failed to load story file: ${error?.message || error}`); process.exit(1); } } else { console.log('📖 Using demo story'); storyData = demo_story_js_1.DEMO_STORY; } // Telemetry flags const telemetryIdx = args.indexOf('--telemetry'); const telemetryKind = telemetryIdx >= 0 ? (args[telemetryIdx + 1] || 'none') : 'none'; const telemetrySampleIdx = args.indexOf('--telemetry-sample'); const telemetrySample = telemetrySampleIdx >= 0 ? Math.max(0, Math.min(1, parseFloat(args[telemetrySampleIdx + 1]))) : undefined; const telemetryFileIdx = args.indexOf('--telemetry-file'); const telemetryPath = telemetryFileIdx >= 0 ? args[telemetryFileIdx + 1] : undefined; const telemetryReport = args.includes('--telemetry-report'); let telemetry; if (telemetryKind && telemetryKind !== 'none') { const adapter = telemetryKind === 'file' ? (0, core_js_2.createTelemetryAdapter)('file', { path: telemetryPath || 'qnce-telemetry.ndjson' }) : (0, core_js_2.createTelemetryAdapter)('console'); telemetry = (0, core_js_2.createTelemetry)({ adapter, enabled: true, sampleRate: telemetrySample ?? (process.env.NODE_ENV === 'production' ? 0 : 0.25), defaultCtx: { engineVersion: '1.3.1', env: process.env.NODE_ENV || 'dev', sessionId: '' } }); } const engine = (0, core_js_1.createQNCEEngine)(storyData, undefined, false, undefined, telemetry ? { telemetry, env: process.env.NODE_ENV || 'dev' } : undefined); // Storage adapter selection const storageIdx = args.indexOf('--storage'); const storageType = storageIdx >= 0 ? args[storageIdx + 1] : undefined; if (storageType) { try { const adapterOptions = {}; const idxPrefix = args.indexOf('--storage-prefix'); if (idxPrefix >= 0) adapterOptions.prefix = args[idxPrefix + 1]; const idxDir = args.indexOf('--storage-dir'); if (idxDir >= 0) adapterOptions.directory = args[idxDir + 1]; const idxDb = args.indexOf('--storage-db'); if (idxDb >= 0) adapterOptions.databaseName = args[idxDb + 1]; const adapter = (0, StorageAdapters_js_1.createStorageAdapter)(storageType, adapterOptions); engine.attachStorageAdapter?.(adapter); console.log(`💾 Storage adapter attached: ${storageType}`); } catch (e) { console.error(`❌ Failed to attach storage adapter '${storageType}': ${e?.message || e}`); process.exit(1); } } // Non-interactive mode for scripting/tests const nonInteractive = args.includes('--non-interactive'); const saveKeyIdx = args.indexOf('--save-key'); const saveKey = saveKeyIdx >= 0 ? args[saveKeyIdx + 1] : undefined; const loadKeyIdx = args.indexOf('--load-key'); const loadKey = loadKeyIdx >= 0 ? args[loadKeyIdx + 1] : undefined; if (nonInteractive) { (async () => { try { if (loadKey) { if (engine.loadFromStorage) { await engine.loadFromStorage(loadKey); console.log(`✅ Loaded state from key '${loadKey}'`); } else { console.warn('⚠️ No storage adapter attached; cannot load'); } } if (saveKey) { if (engine.saveToStorage) { await engine.saveToStorage(saveKey); console.log(`✅ Saved state to key '${saveKey}'`); } else { console.warn('⚠️ No storage adapter attached; cannot save'); } } const summary = { currentNodeId: engine.getState().currentNodeId, }; if (engine.listStorageKeys) { try { summary.storageKeys = await engine.listStorageKeys(); } catch { } } console.log(JSON.stringify(summary)); // If telemetry report requested in non-interactive mode, print it here before exit if (telemetry && telemetryReport) { await telemetry.flush(); const s = telemetry.stats(); console.log('\nTelemetry Report'); console.log('----------------'); console.log(`queued : ${s.queued}`); console.log(`sent : ${s.sent}`); console.log(`dropped:${s.dropped}`); if (typeof s.p50 === 'number' || typeof s.p95 === 'number') { console.log(`p50 send latency: ${s.p50 ?? 'n/a'} ms`); console.log(`p95 send latency: ${s.p95 ?? 'n/a'} ms`); console.log('\nBatch send latency (ms):'); console.log(`p50: ${s.p50 ?? 'n/a'} | p95: ${s.p95 ?? 'n/a'}`); } } process.exit(0); } catch (error) { console.error('❌ Non-interactive run failed:', error?.message || error); process.exit(1); } })(); return; } const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout }); const session = { engine, rl, telemetry }; startInteractiveSession(session).catch((error) => { console.error('❌ Session error:', error.message); process.exit(1); }); // Print telemetry report on exit if (telemetry && telemetryReport) { process.on('beforeExit', async () => { await telemetry.flush(); const s = telemetry.stats(); // Minimal ASCII report console.log('\nTelemetry Report'); console.log('----------------'); console.log(`queued : ${s.queued}`); console.log(`sent : ${s.sent}`); console.log(`dropped:${s.dropped}`); if (typeof s.p50 === 'number' || typeof s.p95 === 'number') { console.log(`p50 send latency: ${s.p50 ?? 'n/a'} ms`); console.log(`p95 send latency: ${s.p95 ?? 'n/a'} ms`); } if (typeof s.p50 === 'number' || typeof s.p95 === 'number') { console.log('\nBatch send latency (ms):'); console.log(`p50: ${s.p50 ?? 'n/a'} | p95: ${s.p95 ?? 'n/a'}`); } }); } } // Run if called directly if (process.argv[1] && process.argv[1].endsWith('play.js')) { main(); } //# sourceMappingURL=play.js.map