Initial release — Crucix Intelligence Engine v2.0.0
26-source OSINT intelligence engine with live Jarvis dashboard, auto-refresh via SSE, optional LLM layer (4 providers), delta/memory system, and Telegram breaking news alerts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
234
server.mjs
Normal file
234
server.mjs
Normal file
@@ -0,0 +1,234 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user