#!/usr/bin/env node // Crucix Intelligence Engine — Dev Server // Serves the Jarvis dashboard, runs sweep cycle, pushes live updates via SSE import express from 'express'; import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { exec } from 'child_process'; import config from './crucix.config.mjs'; import { fullBriefing } from './apis/briefing.mjs'; import { synthesize, generateIdeas } from './dashboard/inject.mjs'; import { MemoryManager } from './lib/delta/index.mjs'; import { createLLMProvider } from './lib/llm/index.mjs'; import { generateLLMIdeas } from './lib/llm/ideas.mjs'; import { TelegramAlerter } from './lib/alerts/telegram.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = __dirname; const RUNS_DIR = join(ROOT, 'runs'); const MEMORY_DIR = join(RUNS_DIR, 'memory'); // Ensure directories exist for (const dir of [RUNS_DIR, MEMORY_DIR, join(MEMORY_DIR, 'cold')]) { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); } // === State === let currentData = null; // Current synthesized dashboard data let lastSweepTime = null; // Timestamp of last sweep let sweepInProgress = false; const startTime = Date.now(); const sseClients = new Set(); // === Delta/Memory === const memory = new MemoryManager(RUNS_DIR); // === LLM + Telegram === const llmProvider = createLLMProvider(config.llm); const telegramAlerter = new TelegramAlerter(config.telegram); if (llmProvider) console.log(`[Crucix] LLM enabled: ${llmProvider.name} (${llmProvider.model})`); if (telegramAlerter.isConfigured) console.log('[Crucix] Telegram alerts enabled'); // === Express Server === const app = express(); app.use(express.static(join(ROOT, 'dashboard/public'))); // Serve jarvis.html as the root page app.get('/', (req, res) => { res.sendFile(join(ROOT, 'dashboard/public/jarvis.html')); }); // API: current data app.get('/api/data', (req, res) => { if (!currentData) return res.status(503).json({ error: 'No data yet — first sweep in progress' }); res.json(currentData); }); // API: health check app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: Math.floor((Date.now() - startTime) / 1000), lastSweep: lastSweepTime, nextSweep: lastSweepTime ? new Date(new Date(lastSweepTime).getTime() + config.refreshIntervalMinutes * 60000).toISOString() : null, sweepInProgress, sourcesOk: currentData?.meta?.sourcesOk || 0, sourcesFailed: currentData?.meta?.sourcesFailed || 0, llmEnabled: !!config.llm.provider, llmProvider: config.llm.provider, telegramEnabled: !!(config.telegram.botToken && config.telegram.chatId), refreshIntervalMinutes: config.refreshIntervalMinutes, }); }); // SSE: live updates app.get('/events', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', }); res.write('data: {"type":"connected"}\n\n'); sseClients.add(res); req.on('close', () => sseClients.delete(res)); }); function broadcast(data) { const msg = `data: ${JSON.stringify(data)}\n\n`; for (const client of sseClients) { try { client.write(msg); } catch { sseClients.delete(client); } } } // === Sweep Cycle === async function runSweepCycle() { if (sweepInProgress) { console.log('[Crucix] Sweep already in progress, skipping'); return; } sweepInProgress = true; broadcast({ type: 'sweep_start', timestamp: new Date().toISOString() }); console.log(`\n${'='.repeat(60)}`); console.log(`[Crucix] Starting sweep at ${new Date().toLocaleTimeString()}`); console.log(`${'='.repeat(60)}`); try { // 1. Run the full briefing sweep const rawData = await fullBriefing(); // 2. Save to runs/latest.json writeFileSync(join(RUNS_DIR, 'latest.json'), JSON.stringify(rawData, null, 2)); lastSweepTime = new Date().toISOString(); // 3. Synthesize into dashboard format console.log('[Crucix] Synthesizing dashboard data...'); const synthesized = await synthesize(rawData); // 4. Delta computation + memory const delta = memory.addRun(synthesized); synthesized.delta = delta; // 5. LLM-powered trade ideas (LLM-only feature) — isolated so failures don't kill sweep if (llmProvider?.isConfigured) { try { console.log('[Crucix] Generating LLM trade ideas...'); const previousIdeas = memory.getLastRun()?.ideas || []; const llmIdeas = await generateLLMIdeas(llmProvider, synthesized, delta, previousIdeas); if (llmIdeas) { synthesized.ideas = llmIdeas; synthesized.ideasSource = 'llm'; console.log(`[Crucix] LLM generated ${llmIdeas.length} ideas`); } else { synthesized.ideas = []; synthesized.ideasSource = 'llm-failed'; } } catch (llmErr) { console.error('[Crucix] LLM ideas failed (non-fatal):', llmErr.message); synthesized.ideas = []; synthesized.ideasSource = 'llm-failed'; } } else { synthesized.ideas = []; synthesized.ideasSource = 'disabled'; } // 6. Telegram alert evaluation (LLM-gated) if (telegramAlerter.isConfigured && llmProvider?.isConfigured && delta?.summary?.criticalChanges > 0) { telegramAlerter.evaluateAndAlert(llmProvider, delta, memory).catch(err => { console.error('[Crucix] Telegram alert error:', err.message); }); } // Prune old alerted signals memory.pruneAlertedSignals(); currentData = synthesized; // 6. Push to all connected browsers broadcast({ type: 'update', data: currentData }); console.log(`[Crucix] Sweep complete — ${currentData.meta.sourcesOk}/${currentData.meta.sourcesQueried} sources OK`); console.log(`[Crucix] ${currentData.ideas.length} ideas (${synthesized.ideasSource}) | ${currentData.news.length} news | ${currentData.newsFeed.length} feed items`); if (delta?.summary) console.log(`[Crucix] Delta: ${delta.summary.totalChanges} changes, ${delta.summary.criticalChanges} critical, direction: ${delta.summary.direction}`); console.log(`[Crucix] Next sweep at ${new Date(Date.now() + config.refreshIntervalMinutes * 60000).toLocaleTimeString()}`); } catch (err) { console.error('[Crucix] Sweep failed:', err.message); broadcast({ type: 'sweep_error', error: err.message }); } finally { sweepInProgress = false; } } // === Startup === async function start() { const port = config.port; console.log(` ╔══════════════════════════════════════════════╗ ║ CRUCIX INTELLIGENCE ENGINE ║ ║ Local Palantir · 26 Sources ║ ╠══════════════════════════════════════════════╣ ║ Dashboard: http://localhost:${port}${' '.repeat(14 - String(port).length)}║ ║ Health: http://localhost:${port}/api/health${' '.repeat(4 - String(port).length)}║ ║ Refresh: Every ${config.refreshIntervalMinutes} min${' '.repeat(20 - String(config.refreshIntervalMinutes).length)}║ ║ LLM: ${(config.llm.provider || 'disabled').padEnd(31)}║ ║ Alerts: ${config.telegram.botToken ? 'Telegram enabled' : 'disabled'}${' '.repeat(config.telegram.botToken ? 14 : 23)}║ ╚══════════════════════════════════════════════╝ `); app.listen(port, () => { console.log(`[Crucix] Server running on http://localhost:${port}`); // Auto-open browser const openCmd = process.platform === 'win32' ? 'start ""' : process.platform === 'darwin' ? 'open' : 'xdg-open'; exec(`${openCmd} "http://localhost:${port}"`, (err) => { if (err) console.log('[Crucix] Could not auto-open browser:', err.message); }); // Try to load existing data first for instant display try { const existing = JSON.parse(readFileSync(join(RUNS_DIR, 'latest.json'), 'utf8')); synthesize(existing).then(data => { currentData = data; console.log('[Crucix] Loaded existing data from runs/latest.json'); broadcast({ type: 'update', data: currentData }); }).catch(() => {}); } catch { /* no existing data */ } // Run first sweep console.log('[Crucix] Running initial sweep...'); runSweepCycle(); // Schedule recurring sweeps setInterval(runSweepCycle, config.refreshIntervalMinutes * 60 * 1000); }); } // Graceful error handling process.on('unhandledRejection', (err) => { console.error('[Crucix] Unhandled rejection:', err.message || err); }); process.on('uncaughtException', (err) => { console.error('[Crucix] Uncaught exception:', err.message || err); }); start();