const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const chokidar = require('chokidar'); const fs = require('fs'); const path = require('path'); const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383)); const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1'; const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST); const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; if (!fs.existsSync(SCREEN_DIR)) { fs.mkdirSync(SCREEN_DIR, { recursive: true }); } // Load frame template and helper script once at startup const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8'); const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8'); const helperInjection = ``; // Detect whether content is a full HTML document or a bare fragment function isFullDocument(html) { const trimmed = html.trimStart().toLowerCase(); return trimmed.startsWith('', content); } // Find the newest .html file in the directory by mtime function getNewestScreen() { const files = fs.readdirSync(SCREEN_DIR) .filter(f => f.endsWith('.html')) .map(f => ({ name: f, path: path.join(SCREEN_DIR, f), mtime: fs.statSync(path.join(SCREEN_DIR, f)).mtime.getTime() })) .sort((a, b) => b.mtime - a.mtime); return files.length > 0 ? files[0].path : null; } const WAITING_PAGE = ` Brainstorm Companion

Brainstorm Companion

Waiting for Claude to push a screen...

`; const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); const clients = new Set(); wss.on('connection', (ws) => { clients.add(ws); ws.on('close', () => clients.delete(ws)); ws.on('message', (data) => { const event = JSON.parse(data.toString()); console.log(JSON.stringify({ source: 'user-event', ...event })); // Write user events to .events file for Claude to read if (event.choice) { const eventsFile = path.join(SCREEN_DIR, '.events'); fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); } }); }); // Serve newest screen with helper.js injected app.get('/', (req, res) => { const screenFile = getNewestScreen(); let html; if (!screenFile) { html = WAITING_PAGE; } else { const raw = fs.readFileSync(screenFile, 'utf-8'); html = isFullDocument(raw) ? raw : wrapInFrame(raw); } // Inject helper script if (html.includes('')) { html = html.replace('', `${helperInjection}\n`); } else { html += helperInjection; } res.type('html').send(html); }); // Watch for new or changed .html files chokidar.watch(SCREEN_DIR, { ignoreInitial: true }) .on('add', (filePath) => { if (filePath.endsWith('.html')) { // Clear events from previous screen const eventsFile = path.join(SCREEN_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); console.log(JSON.stringify({ type: 'screen-added', file: filePath })); clients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'reload' })); } }); } }) .on('change', (filePath) => { if (filePath.endsWith('.html')) { console.log(JSON.stringify({ type: 'screen-updated', file: filePath })); clients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'reload' })); } }); } }); server.listen(PORT, HOST, () => { console.log(JSON.stringify({ type: 'server-started', port: PORT, host: HOST, url_host: URL_HOST, url: `http://${URL_HOST}:${PORT}`, screen_dir: SCREEN_DIR })); });