| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141 |
- 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 = `<script>\n${helperScript}\n</script>`;
- // Detect whether content is a full HTML document or a bare fragment
- function isFullDocument(html) {
- const trimmed = html.trimStart().toLowerCase();
- return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
- }
- // Wrap a content fragment in the frame template
- function wrapInFrame(content) {
- return frameTemplate.replace('<!-- CONTENT -->', 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 = `<!DOCTYPE html>
- <html>
- <head>
- <title>Brainstorm Companion</title>
- <style>
- body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
- h1 { color: #333; }
- p { color: #666; }
- </style>
- </head>
- <body>
- <h1>Brainstorm Companion</h1>
- <p>Waiting for Claude to push a screen...</p>
- </body>
- </html>`;
- 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('</body>')) {
- html = html.replace('</body>', `${helperInjection}\n</body>`);
- } 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
- }));
- });
|