零依赖头脑风暴服务器
Section titled “零依赖头脑风暴服务器”替换 the brainstorm companion 服务器’s vendored node_modules (express, ws, chokidar — 714 tracked files) with a single 零依赖 server.js using only Node.js built-ins.
Motivation
Section titled “Motivation”Vendoring node_modules into the git repo creates a supply chain risk: frozen 依赖 don’t get 安全 patches, 714 files of third-party code are committed without audit, and modifications to vendored code look like normal commits. While the actual risk is low (localhost-only dev 服务器), eliminating it is straightforward.
A single server.js file (~250-300 lines) using http, crypto, fs, and path. The file serves two roles:
- 当 run directly (
node server.js): starts the HTTP/WebSocket 服务器 - 当 必需 (
require('./server.js')): exports WebSocket protocol functions for unit testing
WebSocket Protocol
Section titled “WebSocket Protocol”Implements RFC 6455 for text frames only:
Handshake: Compute Sec-WebSocket-Accept from 客户端’s Sec-WebSocket-Key using SHA-1 + the RFC 6455 magic GUID. Return 101 Switching Protocols.
Frame decoding (客户端 to 服务器): Handle three masked length encodings:
- Small: payload < 126 bytes
- Medium: 126-65535 bytes (16-bit extended)
- Large: > 65535 bytes (64-bit extended)
XOR-unmask payload using 4-byte mask key. Return { opcode, payload, bytesConsumed } or null for incomplete buffers. Reject unmasked frames.
Frame encoding (服务器 to 客户端): Unmasked frames with the same three length encodings.
Opcodes handled: TEXT (0x01), CLOSE (0x08), PING (0x09), PONG (0x0A). Unrecognized opcodes get a close frame with status 1003 (Unsupported Data).
Deliberately skipped: Binary frames, fragmented messages, extensions (permessage-deflate), subprotocols. These are unnecessary for small JSON text messages between localhost clients. Extensions and subprotocols are negotiated in the handshake — by not advertising them, they are never active.
Buffer accumulation: Each connection maintains a buffer. On data, append and loop decodeFrame until it returns null or buffer is empty.
HTTP Server
Section titled “HTTP Server”Three routes:
GET /— Serve newest.htmlfrom screen 目录 by mtime. Detect full documents vs fragments, wrap fragments in frame 模板, inject helper.js. Returntext/html. 当 no.htmlfiles exist, serve a hardcoded waiting page (“Waiting for Claude to push a screen…”) with helper.js injected.GET /files/*— Serve static files from screen 目录 with MIME type lookup from a hardcoded extension map (html, css, js, png, jpg, gif, svg, json). Return 404 if not found.- Everything else — 404.
WebSocket upgrade handled via the 'upgrade' event on the HTTP 服务器, separate from the request handler.
Environment variables (all 可选):
BRAINSTORM_PORT— port to bind (default: random high port 49152-65535)BRAINSTORM_HOST— interface to bind (default:127.0.0.1)BRAINSTORM_URL_HOST— hostname for the URL in startup JSON (default:localhostwhen host is127.0.0.1, otherwise same as host)BRAINSTORM_DIR— screen 目录 path (default:/tmp/brainstorm)
Startup Sequence
Section titled “Startup Sequence”- 创建
SCREEN_DIRif it doesn’t exist (mkdirSyncrecursive) - Load frame 模板 and helper.js from
__dirname - Start HTTP 服务器 on configured host/port
- Start
fs.watchonSCREEN_DIR - On 成功 listen, log
server-startedJSON to stdout:{ type, port, host, url_host, url, screen_dir } - Write the same JSON to
SCREEN_DIR/.server-infoso agents can find connection details when stdout is hidden (背景 execution)
Application-Level WebSocket Messages
Section titled “Application-Level WebSocket Messages”当 a TEXT frame arrives from a 客户端:
- Parse as JSON. 如果 parsing fails, log to stderr and continue.
- Log to stdout as
{ source: 'user-event', ...event }. - 如果 the event contains a
choiceproperty, append the JSON toSCREEN_DIR/.events(one line per event).
File Watching
Section titled “File Watching”fs.watch(SCREEN_DIR) replaces chokidar. On HTML file events:
- On 新 file (
renameevent for a file that exists): delete.eventsfile if present (unlinkSync), logscreen-addedto stdout as JSON - On file change (
changeevent): logscreen-updatedto stdout as JSON (do NOT clear.events) - Both events: send
{ type: 'reload' }to all connected WebSocket clients
Debounce per-filename with ~100ms timeout to prevent duplicate events (common on macOS and Linux).
Error Handling
Section titled “Error Handling”- Malformed JSON from WebSocket clients: log to stderr, continue
- Unhandled opcodes: close with status 1003
- Client disconnects: remove from broadcast set
fs.watch错误: log to stderr, continue- No graceful shutdown logic — shell scripts handle process lifecycle via SIGTERM
What Changes
Section titled “What Changes”| Before | After |
|---|---|
index.js + package.json + package-lock.json + 714 node_modules files | server.js (single file) |
| express, ws, chokidar 依赖 | none |
| No static file serving | /files/* serves from screen 目录 |
What Stays the Same
Section titled “What Stays the Same”helper.js— no changesframe-template.html— no changesstart-server.sh— one-line update:index.jstoserver.jsstop-server.sh— no changesvisual-companion.md— no changes- All 现有 服务器 behavior and external contract
Platform Compatibility
Section titled “Platform Compatibility”server.jsuses only cross-platform Node built-insfs.watchis reliable for single flat 目录 on macOS, Linux, and Windows- Shell scripts require bash (Git Bash on Windows, which is 必需 for Claude Code)
Testing
Section titled “Testing”Unit tests (ws-protocol.test.js): Test WebSocket frame encoding/decoding, handshake computation, and protocol edge cases directly by requiring server.js exports.
Integration tests (server.test.js): Test full 服务器 behavior — HTTP serving, WebSocket communication, file watching, brainstorming workflow. Uses ws npm package as a test-only 客户端 依赖 (not shipped to end users).