Skip to content

零依赖头脑风暴服务器实施计划

由 Markdown 原样翻译并转换为 Astro Starlight MDX 格式。

零依赖头脑风暴服务器实施计划

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)


  • 创建: skills/brainstorming/scripts/server.js — the zero-dep replacement
  • 修改: skills/brainstorming/scripts/start-server.sh:94,100 — change index.js to server.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

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: 提交
Terminal window
git add skills/brainstorming/scripts/server.js
git 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: 提交
Terminal window
git add skills/brainstorming/scripts/server.js
git commit -m "Add HTTP server, WebSocket handling, and file watching to server.js"

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.js to server.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
Terminal window
git rm skills/brainstorming/scripts/index.js
git rm skills/brainstorming/scripts/package.json
git rm skills/brainstorming/scripts/package-lock.json
git 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: 提交
Terminal window
git add skills/brainstorming/scripts/ .gitignore
git commit -m "Remove vendored node_modules, swap to zero-dep server.js"
  • 步骤 1: Start the 服务器 manually
Terminal window
cd skills/brainstorming/scripts
BRAINSTORM_DIR=/tmp/brainstorm-smoke BRAINSTORM_PORT=9876 node server.js

预期: server-started JSON printed with port 9876

预期: Waiting page with “Waiting for Claude to push a screen…”

  • 步骤 3: Write an HTML file to the screen 目录
Terminal window
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
Terminal window
rm -rf /tmp/brainstorm-smoke
-
0:000:00