零依赖头脑风暴服务器实施计划
Section titled “零依赖头脑风暴服务器实施计划”对于 agentic workers: REQUIRED: 使用 superpowers:subagent-driven-development (if subagents 可用) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 替换 the brainstorm 服务器’s vendored node_modules with a single 零依赖 server.js using Node built-ins.
架构: Single file with WebSocket protocol (RFC 6455 text frames), HTTP 服务器 (http module), and file watching (fs.watch). Exports protocol functions for unit testing when 必需 as a module.
Tech Stack: Node.js built-ins only: http, crypto, fs, path
Spec: docs/superpowers/specs/2026-03-11-zero-dep-brainstorm-server-design.md
Existing tests: tests/brainstorm-server/ws-protocol.test.js (unit), tests/brainstorm-server/server.test.js (integration)
File Map
Section titled “File Map”- 创建:
skills/brainstorming/scripts/server.js— the zero-dep replacement - 修改:
skills/brainstorming/scripts/start-server.sh:94,100— changeindex.jstoserver.js - 修改:
.gitignore:6— remove the!skills/brainstorming/scripts/node_modules/exception - 删除:
skills/brainstorming/scripts/index.js - 删除:
skills/brainstorming/scripts/package.json - 删除:
skills/brainstorming/scripts/package-lock.json - 删除:
skills/brainstorming/scripts/node_modules/(714 files) - No changes:
skills/brainstorming/scripts/helper.js,skills/brainstorming/scripts/frame-template.html,skills/brainstorming/scripts/stop-server.sh
Chunk 1: WebSocket Protocol Layer
Section titled “Chunk 1: WebSocket Protocol Layer”Task 1: Implement WebSocket protocol exports
Section titled “Task 1: Implement WebSocket protocol exports”文件:
-
创建:
skills/brainstorming/scripts/server.js -
Test:
tests/brainstorm-server/ws-protocol.test.js(already exists) -
步骤 1: 创建 server.js with OPCODES constant and computeAcceptKey
const crypto = require('crypto');
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function computeAcceptKey(clientKey) { return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');}- 步骤 2: Implement encodeFrame
Server frames are never masked. Three length encodings:
- payload < 126: 2-byte header (FIN+opcode, length)
- 126-65535: 4-byte header (FIN+opcode, 126, 16-bit length)
- > 65535: 10-byte header (FIN+opcode, 127, 64-bit length)
function encodeFrame(opcode, payload) { const fin = 0x80; const len = payload.length; let header;
if (len < 126) { header = Buffer.alloc(2); header[0] = fin | opcode; header[1] = len; } else if (len < 65536) { header = Buffer.alloc(4); header[0] = fin | opcode; header[1] = 126; header.writeUInt16BE(len, 2); } else { header = Buffer.alloc(10); header[0] = fin | opcode; header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2); }
return Buffer.concat([header, payload]);}- 步骤 3: Implement decodeFrame
Client frames are always masked. Returns { opcode, payload, bytesConsumed } or null for incomplete. Throws on unmasked frames.
function decodeFrame(buffer) { if (buffer.length < 2) return null;
const firstByte = buffer[0]; const secondByte = buffer[1]; const opcode = firstByte & 0x0F; const masked = (secondByte & 0x80) !== 0; let payloadLen = secondByte & 0x7F; let offset = 2;
if (!masked) throw new Error('Client frames must be masked');
if (payloadLen === 126) { if (buffer.length < 4) return null; payloadLen = buffer.readUInt16BE(2); offset = 4; } else if (payloadLen === 127) { if (buffer.length < 10) return null; payloadLen = Number(buffer.readBigUInt64BE(2)); offset = 10; }
const maskOffset = offset; const dataOffset = offset + 4; const totalLen = dataOffset + payloadLen; if (buffer.length < totalLen) return null;
const mask = buffer.slice(maskOffset, dataOffset); const data = Buffer.alloc(payloadLen); for (let i = 0; i < payloadLen; i++) { data[i] = buffer[dataOffset + i] ^ mask[i % 4]; }
return { opcode, payload: data, bytesConsumed: totalLen };}- 步骤 4: 添加 module exports at the bottom of the file
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };- 步骤 5: 运行 unit tests
运行: cd tests/brainstorm-server && node ws-protocol.test.js
预期: All tests pass (handshake, encoding, decoding, boundaries, edge cases)
- 步骤 6: 提交
git add skills/brainstorming/scripts/server.jsgit commit -m "Add WebSocket protocol layer for zero-dep brainstorm server"Chunk 2: HTTP Server and Application Logic
Section titled “Chunk 2: HTTP Server and Application Logic”Task 2: 添加 HTTP 服务器, file watching, and WebSocket connection handling
Section titled “Task 2: 添加 HTTP 服务器, file watching, and WebSocket connection handling”文件:
-
修改:
skills/brainstorming/scripts/server.js -
Test:
tests/brainstorm-server/server.test.js(already exists) -
步骤 1: 添加 配置 and constants at top of server.js (after requires)
const http = require('http');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';
const MIME_TYPES = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'};- 步骤 2: 添加 WAITING_PAGE, 模板 加载 at module 范围, and helper functions
Load frameTemplate and helperInjection at module 范围 so they’re accessible to wrapInFrame and handleRequest. They only read files from __dirname (the scripts 目录), which is valid whether the module is 必需 or run directly.
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 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>';
function isFullDocument(html) { const trimmed = html.trimStart().toLowerCase(); return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');}
function wrapInFrame(content) { return frameTemplate.replace('<!-- CONTENT -->', content);}
function getNewestScreen() { const files = fs.readdirSync(SCREEN_DIR) .filter(f => f.endsWith('.html')) .map(f => { const fp = path.join(SCREEN_DIR, f); return { path: fp, mtime: fs.statSync(fp).mtime.getTime() }; }) .sort((a, b) => b.mtime - a.mtime); return files.length > 0 ? files[0].path : null;}- 步骤 3: 添加 HTTP request handler
function handleRequest(req, res) { if (req.method === 'GET' && req.url === '/') { const screenFile = getNewestScreen(); let html = screenFile ? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8')) : WAITING_PAGE;
if (html.includes('</body>')) { html = html.replace('</body>', helperInjection + '\n</body>'); } else { html += helperInjection; }
res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); } else if (req.method === 'GET' && req.url.startsWith('/files/')) { const fileName = req.url.slice(7); // strip '/files/' const filePath = path.join(SCREEN_DIR, path.basename(fileName)); if (!fs.existsSync(filePath)) { res.writeHead(404); res.end('Not found'); return; } const ext = path.extname(filePath).toLowerCase(); const contentType = MIME_TYPES[ext] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': contentType }); res.end(fs.readFileSync(filePath)); } else { res.writeHead(404); res.end('Not found'); }}- 步骤 4: 添加 WebSocket connection handling
const clients = new Set();
function handleUpgrade(req, socket) { const key = req.headers['sec-websocket-key']; if (!key) { socket.destroy(); return; }
const accept = computeAcceptKey(key); socket.write( 'HTTP/1.1 101 Switching Protocols\r\n' + 'Upgrade: websocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n' );
let buffer = Buffer.alloc(0); clients.add(socket);
socket.on('data', (chunk) => { buffer = Buffer.concat([buffer, chunk]); while (buffer.length > 0) { let result; try { result = decodeFrame(buffer); } catch (e) { socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0))); clients.delete(socket); return; } if (!result) break; buffer = buffer.slice(result.bytesConsumed);
switch (result.opcode) { case OPCODES.TEXT: handleMessage(result.payload.toString()); break; case OPCODES.CLOSE: socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0))); clients.delete(socket); return; case OPCODES.PING: socket.write(encodeFrame(OPCODES.PONG, result.payload)); break; case OPCODES.PONG: break; default: // Unsupported opcode — close with 1003 const closeBuf = Buffer.alloc(2); closeBuf.writeUInt16BE(1003); socket.end(encodeFrame(OPCODES.CLOSE, closeBuf)); clients.delete(socket); return; } } });
socket.on('close', () => clients.delete(socket)); socket.on('error', () => clients.delete(socket));}
function handleMessage(text) { let event; try { event = JSON.parse(text); } catch (e) { console.error('Failed to parse WebSocket message:', e.message); return; } console.log(JSON.stringify({ source: 'user-event', ...event })); if (event.choice) { const eventsFile = path.join(SCREEN_DIR, '.events'); fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); }}
function broadcast(msg) { const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg))); for (const socket of clients) { try { socket.write(frame); } catch (e) { clients.delete(socket); } }}- 步骤 5: 添加 debounce timer map
const debounceTimers = new Map();File watching logic is inlined in startServer (步骤 6) to keep watcher lifecycle together with 服务器 lifecycle and include an error handler per spec.
- 步骤 6: 添加 startServer function and conditional main
frameTemplate and helperInjection are already at module 范围 (步骤 2). startServer just creates the screen dir, starts the HTTP 服务器, watcher, and logs startup info.
function startServer() { if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true });
const server = http.createServer(handleRequest); server.on('upgrade', handleUpgrade);
const watcher = fs.watch(SCREEN_DIR, (eventType, filename) => { if (!filename || !filename.endsWith('.html')) return; if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename)); debounceTimers.set(filename, setTimeout(() => { debounceTimers.delete(filename); const filePath = path.join(SCREEN_DIR, filename); if (eventType === 'rename' && fs.existsSync(filePath)) { const eventsFile = path.join(SCREEN_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); console.log(JSON.stringify({ type: 'screen-added', file: filePath })); } else if (eventType === 'change') { console.log(JSON.stringify({ type: 'screen-updated', file: filePath })); } broadcast({ type: 'reload' }); }, 100)); }); watcher.on('error', (err) => console.error('fs.watch error:', err.message));
server.listen(PORT, HOST, () => { const info = JSON.stringify({ type: 'server-started', port: Number(PORT), host: HOST, url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT, screen_dir: SCREEN_DIR }); console.log(info); fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n'); });}
if (require.main === module) { startServer();}- 步骤 7: 运行 integration tests
The test 目录 already has a package.json with ws as a dependency. Install it if needed, then run tests.
运行: cd tests/brainstorm-server && npm install && node server.test.js
预期:所有测试通过
- 步骤 8: 提交
git add skills/brainstorming/scripts/server.jsgit commit -m "Add HTTP server, WebSocket handling, and file watching to server.js"Chunk 3: Swap and Cleanup
Section titled “Chunk 3: Swap and Cleanup”Task 3: 更新 start-server.sh and remove 旧 files
Section titled “Task 3: 更新 start-server.sh and remove 旧 files”文件:
-
修改:
skills/brainstorming/scripts/start-server.sh:94,100 -
修改:
.gitignore:6 -
删除:
skills/brainstorming/scripts/index.js -
删除:
skills/brainstorming/scripts/package.json -
删除:
skills/brainstorming/scripts/package-lock.json -
删除:
skills/brainstorming/scripts/node_modules/(entire 目录) -
步骤 1: 更新 start-server.sh — change
index.jstoserver.js
Two lines to change:
Line 94: env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js
Line 100: nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js > "$LOG_FILE" 2>&1 &
- 步骤 2: 移除 the gitignore exception for node_modules
In .gitignore, delete line 6: !skills/brainstorming/scripts/node_modules/
- 步骤 3: 删除 旧 files
git rm skills/brainstorming/scripts/index.jsgit rm skills/brainstorming/scripts/package.jsongit rm skills/brainstorming/scripts/package-lock.jsongit rm -r skills/brainstorming/scripts/node_modules/- 步骤 4: 运行 both test suites
运行: cd tests/brainstorm-server && node ws-protocol.test.js && node server.test.js
预期:所有测试通过
- 步骤 5: 提交
git add skills/brainstorming/scripts/ .gitignoregit commit -m "Remove vendored node_modules, swap to zero-dep server.js"Task 4: Manual smoke test
Section titled “Task 4: Manual smoke test”- 步骤 1: Start the 服务器 manually
cd skills/brainstorming/scriptsBRAINSTORM_DIR=/tmp/brainstorm-smoke BRAINSTORM_PORT=9876 node server.js预期: server-started JSON printed with port 9876
- 步骤 2: 打开 browser to http://localhost:9876
预期: Waiting page with “Waiting for Claude to push a screen…”
- 步骤 3: Write an HTML file to the screen 目录
echo '<h2>Hello from smoke test</h2>' > /tmp/brainstorm-smoke/test.html预期: Browser reloads and shows “Hello from smoke test” wrapped in frame 模板
- 步骤 4: 验证 WebSocket works — check browser console
打开 browser dev tools. The WebSocket connection should show as connected (no 错误 in console). The frame 模板’s status indicator should show “Connected”.
- 步骤 5: Stop 服务器 with Ctrl-C, clean up
rm -rf /tmp/brainstorm-smoke