index.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. const express = require('express');
  2. const http = require('http');
  3. const WebSocket = require('ws');
  4. const chokidar = require('chokidar');
  5. const fs = require('fs');
  6. const path = require('path');
  7. const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
  8. const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
  9. if (!fs.existsSync(SCREEN_DIR)) {
  10. fs.mkdirSync(SCREEN_DIR, { recursive: true });
  11. }
  12. // Load frame template and helper script once at startup
  13. const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
  14. const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
  15. const helperInjection = `<script>\n${helperScript}\n</script>`;
  16. // Detect whether content is a full HTML document or a bare fragment
  17. function isFullDocument(html) {
  18. const trimmed = html.trimStart().toLowerCase();
  19. return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
  20. }
  21. // Wrap a content fragment in the frame template
  22. function wrapInFrame(content) {
  23. return frameTemplate.replace(
  24. /(<div id="claude-content">)[\s\S]*?(<\/div>\s*<\/div>\s*<div class="feedback-footer">)/,
  25. `$1\n ${content}\n $2`
  26. );
  27. }
  28. // Find the newest .html file in the directory by mtime
  29. function getNewestScreen() {
  30. const files = fs.readdirSync(SCREEN_DIR)
  31. .filter(f => f.endsWith('.html'))
  32. .map(f => ({
  33. name: f,
  34. path: path.join(SCREEN_DIR, f),
  35. mtime: fs.statSync(path.join(SCREEN_DIR, f)).mtime.getTime()
  36. }))
  37. .sort((a, b) => b.mtime - a.mtime);
  38. return files.length > 0 ? files[0].path : null;
  39. }
  40. const WAITING_PAGE = `<!DOCTYPE html>
  41. <html>
  42. <head>
  43. <title>Brainstorm Companion</title>
  44. <style>
  45. body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
  46. h1 { color: #333; }
  47. p { color: #666; }
  48. </style>
  49. </head>
  50. <body>
  51. <h1>Brainstorm Companion</h1>
  52. <p>Waiting for Claude to push a screen...</p>
  53. </body>
  54. </html>`;
  55. const app = express();
  56. const server = http.createServer(app);
  57. const wss = new WebSocket.Server({ server });
  58. const clients = new Set();
  59. wss.on('connection', (ws) => {
  60. clients.add(ws);
  61. ws.on('close', () => clients.delete(ws));
  62. ws.on('message', (data) => {
  63. const event = JSON.parse(data.toString());
  64. console.log(JSON.stringify({ source: 'user-event', ...event }));
  65. });
  66. });
  67. // Serve newest screen with helper.js injected
  68. app.get('/', (req, res) => {
  69. const screenFile = getNewestScreen();
  70. let html;
  71. if (!screenFile) {
  72. html = WAITING_PAGE;
  73. } else {
  74. const raw = fs.readFileSync(screenFile, 'utf-8');
  75. html = isFullDocument(raw) ? raw : wrapInFrame(raw);
  76. }
  77. // Inject helper script
  78. if (html.includes('</body>')) {
  79. html = html.replace('</body>', `${helperInjection}\n</body>`);
  80. } else {
  81. html += helperInjection;
  82. }
  83. res.type('html').send(html);
  84. });
  85. // Watch for new or changed .html files
  86. chokidar.watch(SCREEN_DIR, { ignoreInitial: true })
  87. .on('add', (filePath) => {
  88. if (filePath.endsWith('.html')) {
  89. console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
  90. clients.forEach(ws => {
  91. if (ws.readyState === WebSocket.OPEN) {
  92. ws.send(JSON.stringify({ type: 'reload' }));
  93. }
  94. });
  95. }
  96. })
  97. .on('change', (filePath) => {
  98. if (filePath.endsWith('.html')) {
  99. console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
  100. clients.forEach(ws => {
  101. if (ws.readyState === WebSocket.OPEN) {
  102. ws.send(JSON.stringify({ type: 'reload' }));
  103. }
  104. });
  105. }
  106. });
  107. server.listen(PORT, '127.0.0.1', () => {
  108. console.log(JSON.stringify({
  109. type: 'server-started',
  110. port: PORT,
  111. url: `http://localhost:${PORT}`,
  112. screen_dir: SCREEN_DIR
  113. }));
  114. });