Skip to content

Case Study: Building a WebSocket CLI Server

Project Background

This case study demonstrates how to use Superpowers to build a WebSocket-enabled CLI server. The server supports the brainstorming skill's visual interface, providing:

  • HTTP serving: Visual interface delivery
  • WebSocket communication: Real-time bidirectional communication
  • File watching: Auto-reload interface
  • Event logging: Persist user choices

Workflow Demonstration

Phase 1: Brainstorming

Trigger scenario: Need to build a real-time visualization tool for brainstorming

AI behavior:

  • Asks questions to understand requirements: What features does the server need?
  • Explores technical approaches: WebSocket vs HTTP long-polling?
  • Presents design in sections: Protocol design → File structure → Interface definition

Output: docs/design.md design document

Phase 2: Writing Plans

AI behavior:

  • Breaks design into 2-5 minute tasks
  • Each task includes: file path, specific code, verification steps

Example task breakdown:

markdown
## Task 1: Create Basic HTTP Server
- File: `server.js`
- Steps:
  1. Import http module
  2. Create server listening on port
  3. Serve HTML page
- Verify: curl localhost:3000 returns HTML

## Task 2: Add WebSocket Support
- File: `server.js`
- Steps:
  1. Import ws module
  2. Handle WebSocket upgrade requests
  3. Implement message broadcast
- Verify: Use wscat to connect and send message

Phase 3: TDD Implementation

RED: Write Failing Test

javascript
// server.test.js
await test('serves waiting page when no screens exist', async () => {
  const res = await fetch(`http://localhost:${TEST_PORT}/`);
  assert.strictEqual(res.status, 200);
  assert(res.body.includes('Waiting for Claude'));
});

GREEN: Minimal Implementation

javascript
// server.js
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end('<html><body>Waiting for Claude</body></html>');
});
server.listen(PORT);

REFACTOR: Optimize Code

Add template system, WebSocket support, file watching, etc.

Phase 4: Debugging

Problem: Broadcast fails after WebSocket client disconnects

Diagnosis flow:

  1. Isolate the problem

    bash
    # Start server
    node server.js
    
    # Connect and immediately disconnect
    wscat -c ws://localhost:3000
    # Ctrl+C to disconnect
    
    # Observe error
    # Error: Cannot send to closed WebSocket
  2. Root cause analysis

    • Disconnected client not removed from connection list
    • Broadcast attempts to send to closed connection
  3. Fix

    javascript
    ws.on('close', () => {
      const index = clients.indexOf(ws);
      if (index > -1) clients.splice(index, 1);
    });

Phase 5: Completion

Verification checklist:

  • [ ] All tests pass
  • [ ] No console errors
  • [ ] Documentation updated
  • [ ] Code reviewed

Output: Merge to main branch or create PR

Key Code

HTTP Serving

javascript
const http = require('http');
const fs = require('fs');
const path = require('path');

function serveHtml(dir, port) {
  const server = http.createServer((req, res) => {
    if (req.url !== '/') {
      res.writeHead(404);
      return res.end('Not found');
    }

    // Find newest HTML file
    const files = fs.readdirSync(dir)
      .filter(f => f.endsWith('.html'))
      .map(f => ({
        name: f,
        mtime: fs.statSync(path.join(dir, f)).mtime
      }))
      .sort((a, b) => b.mtime - a.mtime);

    if (files.length === 0) {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      return res.end('<h1>Waiting for Claude...</h1>');
    }

    const content = fs.readFileSync(path.join(dir, files[0].name), 'utf-8');
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end(content);
  });

  server.listen(port);
  console.log(JSON.stringify({ type: 'server-started', port }));
}

WebSocket Communication

javascript
const WebSocket = require('ws');

function setupWebSocket(server, dir) {
  const wss = new WebSocket.Server({ server });
  const clients = new Set();

  wss.on('connection', (ws) => {
    clients.add(ws);

    ws.on('message', (data) => {
      const event = JSON.parse(data.toString());

      // Log user choices to file
      if (event.choice) {
        fs.appendFileSync(
          path.join(dir, '.events'),
          JSON.stringify(event) + '\n'
        );
      }

      // Output to stdout for Claude to read
      console.log(JSON.stringify({ ...event, source: 'user-event' }));
    });

    ws.on('close', () => clients.delete(ws));
  });

  return clients;
}

File Watching

javascript
const fs = require('fs');

function watchFiles(dir, clients) {
  fs.watch(dir, (eventType, filename) => {
    if (!filename?.endsWith('.html')) return;

    // Clear previous event log
    const eventsFile = path.join(dir, '.events');
    if (fs.existsSync(eventsFile)) {
      fs.unlinkSync(eventsFile);
    }

    // Notify all clients to reload
    const message = JSON.stringify({ type: 'reload' });
    clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });

    console.log(JSON.stringify({ type: 'screen-updated', file: filename }));
  });
}

Key Learnings

1. Using the brainstorming Skill

  • AI asks questions proactively instead of writing code directly
  • Design document presented in sections for understanding
  • Technical approaches compared and analyzed

2. Task Breakdown Techniques

  • Each task takes 2-5 minutes
  • Clear verification criteria
  • Task dependencies clearly identified

3. TDD Practice

  • Write tests first, then code
  • RED-GREEN-REFACTOR cycle
  • Tests serve as documentation

4. Systematic Debugging

  • Isolate problem, minimal reproduction
  • Root cause analysis, not surface fixes
  • Verify edge cases after fix

Extension Exercises

  1. Add Authentication: Add token validation for WebSocket connections
  2. Support Multiple Rooms: Implement independent brainstorming sessions
  3. Persist History: Save user choices to a database