From 74a55c0bf4b08aaefe48988f7bc0bf296601bd6b Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 12:56:11 -0800 Subject: [PATCH 01/19] feat: add brainstorm server foundation Create the initial server for the visual brainstorming companion: - Express server with WebSocket support for browser communication - File watcher (chokidar) to detect screen.html changes - Auto-injects helper.js into served HTML for event capture - Binds to localhost only (127.0.0.1) for security - Outputs JSON events to stdout for Claude consumption --- .gitignore | 1 + lib/brainstorm-server/helper.js | 2 + lib/brainstorm-server/index.js | 84 ++ lib/brainstorm-server/package-lock.json | 1036 +++++++++++++++++++++++ lib/brainstorm-server/package.json | 11 + 5 files changed, 1134 insertions(+) create mode 100644 lib/brainstorm-server/helper.js create mode 100644 lib/brainstorm-server/index.js create mode 100644 lib/brainstorm-server/package-lock.json create mode 100644 lib/brainstorm-server/package.json diff --git a/.gitignore b/.gitignore index 573cae048..554c45d5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .worktrees/ .private-journal/ .claude/ +node_modules/ diff --git a/lib/brainstorm-server/helper.js b/lib/brainstorm-server/helper.js new file mode 100644 index 000000000..fda7cd1b4 --- /dev/null +++ b/lib/brainstorm-server/helper.js @@ -0,0 +1,2 @@ +// Placeholder - will be implemented in Task 2 +// This file captures user interactions and sends them via WebSocket diff --git a/lib/brainstorm-server/index.js b/lib/brainstorm-server/index.js new file mode 100644 index 000000000..dc8d50f7e --- /dev/null +++ b/lib/brainstorm-server/index.js @@ -0,0 +1,84 @@ +const express = require('express'); +const http = require('http'); +const WebSocket = require('ws'); +const chokidar = require('chokidar'); +const fs = require('fs'); +const path = require('path'); + +const PORT = process.env.BRAINSTORM_PORT || 3333; +const SCREEN_FILE = process.env.BRAINSTORM_SCREEN || '/tmp/brainstorm/screen.html'; +const SCREEN_DIR = path.dirname(SCREEN_FILE); + +// Ensure screen directory exists +if (!fs.existsSync(SCREEN_DIR)) { + fs.mkdirSync(SCREEN_DIR, { recursive: true }); +} + +// Create default screen if none exists +if (!fs.existsSync(SCREEN_FILE)) { + fs.writeFileSync(SCREEN_FILE, ` + + + Brainstorm Companion + + + +

Brainstorm Companion

+

Waiting for Claude to push a screen...

+ +`); +} + +const app = express(); +const server = http.createServer(app); +const wss = new WebSocket.Server({ server }); + +// Track connected browsers for reload notifications +const clients = new Set(); + +wss.on('connection', (ws) => { + clients.add(ws); + ws.on('close', () => clients.delete(ws)); + + ws.on('message', (data) => { + // User interaction event - write to stdout for Claude + const event = JSON.parse(data.toString()); + console.log(JSON.stringify({ type: 'user-event', ...event })); + }); +}); + +// Serve current screen with helper.js injected +app.get('/', (req, res) => { + let html = fs.readFileSync(SCREEN_FILE, 'utf-8'); + + // Inject helper script before + const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8'); + const injection = ``; + + if (html.includes('')) { + html = html.replace('', `${injection}\n`); + } else { + html += injection; + } + + res.type('html').send(html); +}); + +// Watch for screen file changes +chokidar.watch(SCREEN_FILE).on('change', () => { + console.log(JSON.stringify({ type: 'screen-updated', file: SCREEN_FILE })); + // Notify all browsers to reload + clients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'reload' })); + } + }); +}); + +server.listen(PORT, '127.0.0.1', () => { + console.log(JSON.stringify({ type: 'server-started', port: PORT, url: `http://localhost:${PORT}` })); +}); diff --git a/lib/brainstorm-server/package-lock.json b/lib/brainstorm-server/package-lock.json new file mode 100644 index 000000000..a529aa359 --- /dev/null +++ b/lib/brainstorm-server/package-lock.json @@ -0,0 +1,1036 @@ +{ + "name": "brainstorm-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "brainstorm-server", + "version": "1.0.0", + "dependencies": { + "chokidar": "^3.5.3", + "express": "^4.18.2", + "ws": "^8.14.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/lib/brainstorm-server/package.json b/lib/brainstorm-server/package.json new file mode 100644 index 000000000..87687687e --- /dev/null +++ b/lib/brainstorm-server/package.json @@ -0,0 +1,11 @@ +{ + "name": "brainstorm-server", + "version": "1.0.0", + "description": "Visual brainstorming companion server for Claude Code", + "main": "index.js", + "dependencies": { + "chokidar": "^3.5.3", + "express": "^4.18.2", + "ws": "^8.14.2" + } +} From 333eaa328114f91ba9f6dee994226f6bd33351de Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 12:59:17 -0800 Subject: [PATCH 02/19] feat: add browser helper library for event capture --- lib/brainstorm-server/helper.js | 99 ++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/lib/brainstorm-server/helper.js b/lib/brainstorm-server/helper.js index fda7cd1b4..77ca052be 100644 --- a/lib/brainstorm-server/helper.js +++ b/lib/brainstorm-server/helper.js @@ -1,2 +1,97 @@ -// Placeholder - will be implemented in Task 2 -// This file captures user interactions and sends them via WebSocket +(function() { + const WS_URL = 'ws://' + window.location.host; + let ws = null; + let eventQueue = []; + + function connect() { + ws = new WebSocket(WS_URL); + + ws.onopen = () => { + // Send any queued events + eventQueue.forEach(e => ws.send(JSON.stringify(e))); + eventQueue = []; + }; + + ws.onmessage = (msg) => { + const data = JSON.parse(msg.data); + if (data.type === 'reload') { + window.location.reload(); + } + }; + + ws.onclose = () => { + // Reconnect after 1 second + setTimeout(connect, 1000); + }; + } + + function send(event) { + event.timestamp = Date.now(); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(event)); + } else { + eventQueue.push(event); + } + } + + // Auto-capture clicks on interactive elements + document.addEventListener('click', (e) => { + const target = e.target.closest('button, a, [data-choice], [role="button"], input[type="submit"]'); + if (!target) return; + + // Don't capture regular link navigation + if (target.tagName === 'A' && !target.dataset.choice) return; + + e.preventDefault(); + + send({ + type: 'click', + text: target.textContent.trim(), + choice: target.dataset.choice || null, + id: target.id || null, + className: target.className || null + }); + }); + + // Auto-capture form submissions + document.addEventListener('submit', (e) => { + e.preventDefault(); + const form = e.target; + const formData = new FormData(form); + const data = {}; + formData.forEach((value, key) => { data[key] = value; }); + + send({ + type: 'submit', + formId: form.id || null, + formName: form.name || null, + data: data + }); + }); + + // Auto-capture input changes (debounced) + let inputTimeout = null; + document.addEventListener('input', (e) => { + const target = e.target; + if (!target.matches('input, textarea, select')) return; + + clearTimeout(inputTimeout); + inputTimeout = setTimeout(() => { + send({ + type: 'input', + name: target.name || null, + id: target.id || null, + value: target.value, + inputType: target.type || target.tagName.toLowerCase() + }); + }, 500); // 500ms debounce + }); + + // Expose for explicit use if needed + window.brainstorm = { + send: send, + choice: (value, metadata = {}) => send({ type: 'choice', value, ...metadata }) + }; + + connect(); +})(); From c536926f03330018634ea9a3bd7d32f9025d8f3f Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 13:03:01 -0800 Subject: [PATCH 03/19] fix: ensure user-event type is preserved in WebSocket message output The spread operator order was causing incoming event types to overwrite the user-event type marker. --- lib/brainstorm-server/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/brainstorm-server/index.js b/lib/brainstorm-server/index.js index dc8d50f7e..0c48e2d23 100644 --- a/lib/brainstorm-server/index.js +++ b/lib/brainstorm-server/index.js @@ -47,7 +47,7 @@ wss.on('connection', (ws) => { ws.on('message', (data) => { // User interaction event - write to stdout for Claude const event = JSON.parse(data.toString()); - console.log(JSON.stringify({ type: 'user-event', ...event })); + console.log(JSON.stringify({ ...event, type: 'user-event' })); }); }); From 7e86703081d4a605fcf97a3d10a4423c70c8cc9d Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 13:03:04 -0800 Subject: [PATCH 04/19] test: add brainstorm server integration tests --- tests/brainstorm-server/package-lock.json | 36 ++++++++ tests/brainstorm-server/package.json | 10 ++ tests/brainstorm-server/server.test.js | 106 ++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 tests/brainstorm-server/package-lock.json create mode 100644 tests/brainstorm-server/package.json create mode 100644 tests/brainstorm-server/server.test.js diff --git a/tests/brainstorm-server/package-lock.json b/tests/brainstorm-server/package-lock.json new file mode 100644 index 000000000..383c6ff0f --- /dev/null +++ b/tests/brainstorm-server/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "brainstorm-server-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "brainstorm-server-tests", + "version": "1.0.0", + "dependencies": { + "ws": "^8.19.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/tests/brainstorm-server/package.json b/tests/brainstorm-server/package.json new file mode 100644 index 000000000..a089f03c7 --- /dev/null +++ b/tests/brainstorm-server/package.json @@ -0,0 +1,10 @@ +{ + "name": "brainstorm-server-tests", + "version": "1.0.0", + "scripts": { + "test": "node server.test.js" + }, + "dependencies": { + "ws": "^8.19.0" + } +} diff --git a/tests/brainstorm-server/server.test.js b/tests/brainstorm-server/server.test.js new file mode 100644 index 000000000..db1ebbff7 --- /dev/null +++ b/tests/brainstorm-server/server.test.js @@ -0,0 +1,106 @@ +const { spawn } = require('child_process'); +const http = require('http'); +const WebSocket = require('ws'); +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +const SERVER_PATH = path.join(__dirname, '../../lib/brainstorm-server/index.js'); +const TEST_PORT = 3334; +const TEST_SCREEN = '/tmp/brainstorm-test/screen.html'; + +// Clean up test directory +function cleanup() { + if (fs.existsSync(path.dirname(TEST_SCREEN))) { + fs.rmSync(path.dirname(TEST_SCREEN), { recursive: true }); + } +} + +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function fetch(url) { + return new Promise((resolve, reject) => { + http.get(url, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve({ status: res.statusCode, body: data })); + }).on('error', reject); + }); +} + +async function runTests() { + cleanup(); + + // Start server + const server = spawn('node', [SERVER_PATH], { + env: { ...process.env, BRAINSTORM_PORT: TEST_PORT, BRAINSTORM_SCREEN: TEST_SCREEN } + }); + + let stdout = ''; + server.stdout.on('data', (data) => { stdout += data.toString(); }); + server.stderr.on('data', (data) => { console.error('Server stderr:', data.toString()); }); + + await sleep(1000); // Wait for server to start + + try { + // Test 1: Server starts and outputs JSON + console.log('Test 1: Server startup message'); + assert(stdout.includes('server-started'), 'Should output server-started'); + assert(stdout.includes(TEST_PORT.toString()), 'Should include port'); + console.log(' PASS'); + + // Test 2: GET / returns HTML with helper injected + console.log('Test 2: Serves HTML with helper injected'); + const res = await fetch(`http://localhost:${TEST_PORT}/`); + assert.strictEqual(res.status, 200); + assert(res.body.includes('brainstorm'), 'Should include brainstorm content'); + assert(res.body.includes('WebSocket'), 'Should have helper.js injected'); + console.log(' PASS'); + + // Test 3: WebSocket connection and event relay + console.log('Test 3: WebSocket relays events to stdout'); + stdout = ''; // Reset stdout capture + const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); + await new Promise(resolve => ws.on('open', resolve)); + + ws.send(JSON.stringify({ type: 'click', text: 'Test Button' })); + await sleep(300); + + assert(stdout.includes('user-event'), 'Should relay user events'); + assert(stdout.includes('Test Button'), 'Should include event data'); + ws.close(); + console.log(' PASS'); + + // Test 4: File change triggers reload notification + console.log('Test 4: File change notifies browsers'); + const ws2 = new WebSocket(`ws://localhost:${TEST_PORT}`); + await new Promise(resolve => ws2.on('open', resolve)); + + let gotReload = false; + ws2.on('message', (data) => { + const msg = JSON.parse(data.toString()); + if (msg.type === 'reload') gotReload = true; + }); + + // Modify the screen file + fs.writeFileSync(TEST_SCREEN, 'Updated'); + await sleep(500); + + assert(gotReload, 'Should send reload message on file change'); + ws2.close(); + console.log(' PASS'); + + console.log('\nAll tests passed!'); + + } finally { + server.kill(); + cleanup(); + } +} + +runTests().catch(err => { + console.error('Test failed:', err); + process.exit(1); +}); From cc585ad4d5bc68f1c3204efdae19ea7ba8b960bb Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 13:06:12 -0800 Subject: [PATCH 05/19] feat: add visual companion to brainstorming skill --- skills/brainstorming/SKILL.md | 20 +++++++ skills/brainstorming/visual-companion.md | 72 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 skills/brainstorming/visual-companion.md diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 2fd19ba1e..717e3c2dc 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -52,3 +52,23 @@ Start by understanding the current project context, then ask questions one at a - **Explore alternatives** - Always propose 2-3 approaches before settling - **Incremental validation** - Present design in sections, validate each - **Be flexible** - Go back and clarify when something doesn't make sense + +## Visual Companion (Optional) + +When brainstorming involves visual elements - UI mockups, wireframes, interactive prototypes - use the browser-based visual companion. + +**When to use:** +- Presenting UI/UX options that benefit from visual comparison +- Showing wireframes or layout options +- Gathering structured feedback (ratings, forms) +- Prototyping click interactions + +**How it works:** +1. Start the server as a background job +2. Tell user to open http://localhost:3333 +3. Write HTML to `/tmp/brainstorm/screen.html` (auto-refreshes) +4. Check background task output for user interactions + +The terminal remains the primary conversation interface. The browser is a visual aid. + +**Reference:** See `visual-companion.md` in this skill directory for HTML patterns and API details. diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md new file mode 100644 index 000000000..9d35f460f --- /dev/null +++ b/skills/brainstorming/visual-companion.md @@ -0,0 +1,72 @@ +# Visual Companion Reference + +## Starting the Server + +Run as a background job: + +```bash +node ${PLUGIN_ROOT}/lib/brainstorm-server/index.js +``` + +Tell the user: "I've started a visual companion at http://localhost:3333 - open it in a browser." + +## Pushing Screens + +Write HTML to `/tmp/brainstorm/screen.html`. The server watches this file and auto-refreshes the browser. + +## Reading User Responses + +Check the background task output for JSON events: + +```json +{"type":"user-event","type":"click","text":"Option A","choice":"optionA","timestamp":1234567890} +{"type":"user-event","type":"submit","data":{"notes":"My feedback"},"timestamp":1234567891} +``` + +Event types: +- **click**: User clicked button or `data-choice` element +- **submit**: User submitted form (includes all form data) +- **input**: User typed in field (debounced 500ms) + +## HTML Patterns + +### Choice Cards + +```html +
+ + +
+``` + +### Interactive Mockup + +```html +
+
App Header
+ +
Content
+
+``` + +### Form with Notes + +```html +
+ + + +
+``` + +### Explicit JavaScript + +```html + +``` From 15d0f2a8f623a7179bdf78ec3d63e30ca87246b4 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 13:08:18 -0800 Subject: [PATCH 06/19] fix: correct visual companion documentation issues --- skills/brainstorming/visual-companion.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 9d35f460f..45a25c26d 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -5,7 +5,7 @@ Run as a background job: ```bash -node ${PLUGIN_ROOT}/lib/brainstorm-server/index.js +node ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/index.js ``` Tell the user: "I've started a visual companion at http://localhost:3333 - open it in a browser." @@ -19,14 +19,15 @@ Write HTML to `/tmp/brainstorm/screen.html`. The server watches this file and au Check the background task output for JSON events: ```json -{"type":"user-event","type":"click","text":"Option A","choice":"optionA","timestamp":1234567890} -{"type":"user-event","type":"submit","data":{"notes":"My feedback"},"timestamp":1234567891} +{"text":"Option A","choice":"optionA","timestamp":1234567890,"type":"user-event"} +{"data":{"notes":"My feedback"},"timestamp":1234567891,"type":"user-event"} ``` Event types: - **click**: User clicked button or `data-choice` element - **submit**: User submitted form (includes all form data) - **input**: User typed in field (debounced 500ms) +- **choice**: Explicit choice via `brainstorm.choice()` call ## HTML Patterns From fccb5b4b8f7b46c4fd37ddd6b9d4b82db50bf325 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 13:11:53 -0800 Subject: [PATCH 07/19] fix: preserve original event type, use source field for wrapper --- lib/brainstorm-server/index.js | 2 +- skills/brainstorming/visual-companion.md | 4 ++-- tests/brainstorm-server/server.test.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/brainstorm-server/index.js b/lib/brainstorm-server/index.js index 0c48e2d23..8468ad5ea 100644 --- a/lib/brainstorm-server/index.js +++ b/lib/brainstorm-server/index.js @@ -47,7 +47,7 @@ wss.on('connection', (ws) => { ws.on('message', (data) => { // User interaction event - write to stdout for Claude const event = JSON.parse(data.toString()); - console.log(JSON.stringify({ ...event, type: 'user-event' })); + console.log(JSON.stringify({ source: 'user-event', ...event })); }); }); diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 45a25c26d..a4a9f9071 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -19,8 +19,8 @@ Write HTML to `/tmp/brainstorm/screen.html`. The server watches this file and au Check the background task output for JSON events: ```json -{"text":"Option A","choice":"optionA","timestamp":1234567890,"type":"user-event"} -{"data":{"notes":"My feedback"},"timestamp":1234567891,"type":"user-event"} +{"source":"user-event","type":"click","text":"Option A","choice":"optionA","timestamp":1234567890} +{"source":"user-event","type":"submit","data":{"notes":"My feedback"},"timestamp":1234567891} ``` Event types: diff --git a/tests/brainstorm-server/server.test.js b/tests/brainstorm-server/server.test.js index db1ebbff7..d7c60fcba 100644 --- a/tests/brainstorm-server/server.test.js +++ b/tests/brainstorm-server/server.test.js @@ -68,7 +68,7 @@ async function runTests() { ws.send(JSON.stringify({ type: 'click', text: 'Test Button' })); await sleep(300); - assert(stdout.includes('user-event'), 'Should relay user events'); + assert(stdout.includes('"source":"user-event"'), 'Should relay user events with source field'); assert(stdout.includes('Test Button'), 'Should include event data'); ws.close(); console.log(' PASS'); From 209fcec3b909f4e70ebfd2b6985a5fae96634abc Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 15:13:23 -0800 Subject: [PATCH 08/19] docs: add visual brainstorming implementation plan --- docs/plans/2026-01-17-visual-brainstorming.md | 571 ++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 docs/plans/2026-01-17-visual-brainstorming.md diff --git a/docs/plans/2026-01-17-visual-brainstorming.md b/docs/plans/2026-01-17-visual-brainstorming.md new file mode 100644 index 000000000..34371231e --- /dev/null +++ b/docs/plans/2026-01-17-visual-brainstorming.md @@ -0,0 +1,571 @@ +# Visual Brainstorming Companion Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Give Claude a browser-based visual companion for brainstorming sessions - show mockups, prototypes, and interactive choices alongside terminal conversation. + +**Architecture:** Claude writes HTML to a temp file. A local Node.js server watches that file and serves it with an auto-injected helper library. User interactions flow via WebSocket to server stdout, which Claude sees in background task output. + +**Tech Stack:** Node.js, Express, ws (WebSocket), chokidar (file watching) + +--- + +## Task 1: Create the Server Foundation + +**Files:** +- Create: `lib/brainstorm-server/index.js` +- Create: `lib/brainstorm-server/package.json` + +**Step 1: Create package.json** + +```json +{ + "name": "brainstorm-server", + "version": "1.0.0", + "description": "Visual brainstorming companion server for Claude Code", + "main": "index.js", + "dependencies": { + "chokidar": "^3.5.3", + "express": "^4.18.2", + "ws": "^8.14.2" + } +} +``` + +**Step 2: Create minimal server that starts** + +```javascript +const express = require('express'); +const http = require('http'); +const WebSocket = require('ws'); +const chokidar = require('chokidar'); +const fs = require('fs'); +const path = require('path'); + +const PORT = process.env.BRAINSTORM_PORT || 3333; +const SCREEN_FILE = process.env.BRAINSTORM_SCREEN || '/tmp/brainstorm/screen.html'; +const SCREEN_DIR = path.dirname(SCREEN_FILE); + +// Ensure screen directory exists +if (!fs.existsSync(SCREEN_DIR)) { + fs.mkdirSync(SCREEN_DIR, { recursive: true }); +} + +// Create default screen if none exists +if (!fs.existsSync(SCREEN_FILE)) { + fs.writeFileSync(SCREEN_FILE, ` + + + Brainstorm Companion + + + +

Brainstorm Companion

+

Waiting for Claude to push a screen...

+ +`); +} + +const app = express(); +const server = http.createServer(app); +const wss = new WebSocket.Server({ server }); + +// Track connected browsers for reload notifications +const clients = new Set(); + +wss.on('connection', (ws) => { + clients.add(ws); + ws.on('close', () => clients.delete(ws)); + + ws.on('message', (data) => { + // User interaction event - write to stdout for Claude + const event = JSON.parse(data.toString()); + console.log(JSON.stringify({ type: 'user-event', ...event })); + }); +}); + +// Serve current screen with helper.js injected +app.get('/', (req, res) => { + let html = fs.readFileSync(SCREEN_FILE, 'utf-8'); + + // Inject helper script before + const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8'); + const injection = ``; + + if (html.includes('')) { + html = html.replace('', `${injection}\n`); + } else { + html += injection; + } + + res.type('html').send(html); +}); + +// Watch for screen file changes +chokidar.watch(SCREEN_FILE).on('change', () => { + console.log(JSON.stringify({ type: 'screen-updated', file: SCREEN_FILE })); + // Notify all browsers to reload + clients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'reload' })); + } + }); +}); + +server.listen(PORT, '127.0.0.1', () => { + console.log(JSON.stringify({ type: 'server-started', port: PORT, url: `http://localhost:${PORT}` })); +}); +``` + +**Step 3: Run npm install** + +Run: `cd lib/brainstorm-server && npm install` +Expected: Dependencies installed + +**Step 4: Test server starts** + +Run: `cd lib/brainstorm-server && timeout 3 node index.js || true` +Expected: See JSON with `server-started` and port info + +**Step 5: Commit** + +```bash +git add lib/brainstorm-server/ +git commit -m "feat: add brainstorm server foundation" +``` + +--- + +## Task 2: Create the Helper Library + +**Files:** +- Create: `lib/brainstorm-server/helper.js` + +**Step 1: Create helper.js with event auto-capture** + +```javascript +(function() { + const WS_URL = 'ws://' + window.location.host; + let ws = null; + let eventQueue = []; + + function connect() { + ws = new WebSocket(WS_URL); + + ws.onopen = () => { + // Send any queued events + eventQueue.forEach(e => ws.send(JSON.stringify(e))); + eventQueue = []; + }; + + ws.onmessage = (msg) => { + const data = JSON.parse(msg.data); + if (data.type === 'reload') { + window.location.reload(); + } + }; + + ws.onclose = () => { + // Reconnect after 1 second + setTimeout(connect, 1000); + }; + } + + function send(event) { + event.timestamp = Date.now(); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(event)); + } else { + eventQueue.push(event); + } + } + + // Auto-capture clicks on interactive elements + document.addEventListener('click', (e) => { + const target = e.target.closest('button, a, [data-choice], [role="button"], input[type="submit"]'); + if (!target) return; + + // Don't capture regular link navigation + if (target.tagName === 'A' && !target.dataset.choice) return; + + e.preventDefault(); + + send({ + type: 'click', + text: target.textContent.trim(), + choice: target.dataset.choice || null, + id: target.id || null, + className: target.className || null + }); + }); + + // Auto-capture form submissions + document.addEventListener('submit', (e) => { + e.preventDefault(); + const form = e.target; + const formData = new FormData(form); + const data = {}; + formData.forEach((value, key) => { data[key] = value; }); + + send({ + type: 'submit', + formId: form.id || null, + formName: form.name || null, + data: data + }); + }); + + // Auto-capture input changes (debounced) + let inputTimeout = null; + document.addEventListener('input', (e) => { + const target = e.target; + if (!target.matches('input, textarea, select')) return; + + clearTimeout(inputTimeout); + inputTimeout = setTimeout(() => { + send({ + type: 'input', + name: target.name || null, + id: target.id || null, + value: target.value, + inputType: target.type || target.tagName.toLowerCase() + }); + }, 500); // 500ms debounce + }); + + // Expose for explicit use if needed + window.brainstorm = { + send: send, + choice: (value, metadata = {}) => send({ type: 'choice', value, ...metadata }) + }; + + connect(); +})(); +``` + +**Step 2: Verify helper.js is syntactically valid** + +Run: `node -c lib/brainstorm-server/helper.js` +Expected: No syntax errors + +**Step 3: Commit** + +```bash +git add lib/brainstorm-server/helper.js +git commit -m "feat: add browser helper library for event capture" +``` + +--- + +## Task 3: Write Tests for the Server + +**Files:** +- Create: `tests/brainstorm-server/server.test.js` +- Create: `tests/brainstorm-server/package.json` + +**Step 1: Create test package.json** + +```json +{ + "name": "brainstorm-server-tests", + "version": "1.0.0", + "scripts": { + "test": "node server.test.js" + } +} +``` + +**Step 2: Write server tests** + +```javascript +const { spawn } = require('child_process'); +const http = require('http'); +const WebSocket = require('ws'); +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +const SERVER_PATH = path.join(__dirname, '../../lib/brainstorm-server/index.js'); +const TEST_PORT = 3334; +const TEST_SCREEN = '/tmp/brainstorm-test/screen.html'; + +// Clean up test directory +function cleanup() { + if (fs.existsSync(path.dirname(TEST_SCREEN))) { + fs.rmSync(path.dirname(TEST_SCREEN), { recursive: true }); + } +} + +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function fetch(url) { + return new Promise((resolve, reject) => { + http.get(url, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve({ status: res.statusCode, body: data })); + }).on('error', reject); + }); +} + +async function runTests() { + cleanup(); + + // Start server + const server = spawn('node', [SERVER_PATH], { + env: { ...process.env, BRAINSTORM_PORT: TEST_PORT, BRAINSTORM_SCREEN: TEST_SCREEN } + }); + + let stdout = ''; + server.stdout.on('data', (data) => { stdout += data.toString(); }); + server.stderr.on('data', (data) => { console.error('Server stderr:', data.toString()); }); + + await sleep(1000); // Wait for server to start + + try { + // Test 1: Server starts and outputs JSON + console.log('Test 1: Server startup message'); + assert(stdout.includes('server-started'), 'Should output server-started'); + assert(stdout.includes(TEST_PORT.toString()), 'Should include port'); + console.log(' PASS'); + + // Test 2: GET / returns HTML with helper injected + console.log('Test 2: Serves HTML with helper injected'); + const res = await fetch(`http://localhost:${TEST_PORT}/`); + assert.strictEqual(res.status, 200); + assert(res.body.includes('brainstorm'), 'Should include brainstorm content'); + assert(res.body.includes('WebSocket'), 'Should have helper.js injected'); + console.log(' PASS'); + + // Test 3: WebSocket connection and event relay + console.log('Test 3: WebSocket relays events to stdout'); + stdout = ''; // Reset stdout capture + const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); + await new Promise(resolve => ws.on('open', resolve)); + + ws.send(JSON.stringify({ type: 'click', text: 'Test Button' })); + await sleep(100); + + assert(stdout.includes('user-event'), 'Should relay user events'); + assert(stdout.includes('Test Button'), 'Should include event data'); + ws.close(); + console.log(' PASS'); + + // Test 4: File change triggers reload notification + console.log('Test 4: File change notifies browsers'); + const ws2 = new WebSocket(`ws://localhost:${TEST_PORT}`); + await new Promise(resolve => ws2.on('open', resolve)); + + let gotReload = false; + ws2.on('message', (data) => { + const msg = JSON.parse(data.toString()); + if (msg.type === 'reload') gotReload = true; + }); + + // Modify the screen file + fs.writeFileSync(TEST_SCREEN, 'Updated'); + await sleep(500); + + assert(gotReload, 'Should send reload message on file change'); + ws2.close(); + console.log(' PASS'); + + console.log('\nAll tests passed!'); + + } finally { + server.kill(); + cleanup(); + } +} + +runTests().catch(err => { + console.error('Test failed:', err); + process.exit(1); +}); +``` + +**Step 3: Run tests** + +Run: `cd tests/brainstorm-server && npm install ws && node server.test.js` +Expected: All tests pass + +**Step 4: Commit** + +```bash +git add tests/brainstorm-server/ +git commit -m "test: add brainstorm server integration tests" +``` + +--- + +## Task 4: Add Visual Companion to Brainstorming Skill + +**Files:** +- Modify: `skills/brainstorming/SKILL.md` +- Create: `skills/brainstorming/visual-companion.md` (supporting doc) + +**Step 1: Create the supporting documentation** + +Create `skills/brainstorming/visual-companion.md`: + +```markdown +# Visual Companion Reference + +## Starting the Server + +Run as a background job: + +```bash +node ${PLUGIN_ROOT}/lib/brainstorm-server/index.js +``` + +Tell the user: "I've started a visual companion at http://localhost:3333 - open it in a browser." + +## Pushing Screens + +Write HTML to `/tmp/brainstorm/screen.html`. The server watches this file and auto-refreshes the browser. + +## Reading User Responses + +Check the background task output for JSON events: + +```json +{"type":"user-event","type":"click","text":"Option A","choice":"optionA","timestamp":1234567890} +{"type":"user-event","type":"submit","data":{"notes":"My feedback"},"timestamp":1234567891} +``` + +Event types: +- **click**: User clicked button or `data-choice` element +- **submit**: User submitted form (includes all form data) +- **input**: User typed in field (debounced 500ms) + +## HTML Patterns + +### Choice Cards + +```html +
+ + +
+``` + +### Interactive Mockup + +```html +
+
App Header
+ +
Content
+
+``` + +### Form with Notes + +```html +
+ + + +
+``` + +### Explicit JavaScript + +```html + +``` +``` + +**Step 2: Add visual companion section to brainstorming skill** + +Add after "Key Principles" in `skills/brainstorming/SKILL.md`: + +```markdown + +## Visual Companion (Optional) + +When brainstorming involves visual elements - UI mockups, wireframes, interactive prototypes - use the browser-based visual companion. + +**When to use:** +- Presenting UI/UX options that benefit from visual comparison +- Showing wireframes or layout options +- Gathering structured feedback (ratings, forms) +- Prototyping click interactions + +**How it works:** +1. Start the server as a background job +2. Tell user to open http://localhost:3333 +3. Write HTML to `/tmp/brainstorm/screen.html` (auto-refreshes) +4. Check background task output for user interactions + +The terminal remains the primary conversation interface. The browser is a visual aid. + +**Reference:** See `visual-companion.md` in this skill directory for HTML patterns and API details. +``` + +**Step 3: Verify the edits** + +Run: `grep -A5 "Visual Companion" skills/brainstorming/SKILL.md` +Expected: Shows the new section + +**Step 4: Commit** + +```bash +git add skills/brainstorming/ +git commit -m "feat: add visual companion to brainstorming skill" +``` + +--- + +## Task 5: Add Server to Plugin Ignore (Optional Cleanup) + +**Files:** +- Check if `.gitignore` needs node_modules exclusion for lib/brainstorm-server + +**Step 1: Check current gitignore** + +Run: `cat .gitignore 2>/dev/null || echo "No .gitignore"` + +**Step 2: Add node_modules if needed** + +If not already present, add: +``` +lib/brainstorm-server/node_modules/ +``` + +**Step 3: Commit if changed** + +```bash +git add .gitignore +git commit -m "chore: ignore brainstorm-server node_modules" +``` + +--- + +## Summary + +After completing all tasks: + +1. **Server** at `lib/brainstorm-server/` - Node.js server that watches HTML file and relays events +2. **Helper library** auto-injected - captures clicks, forms, inputs +3. **Tests** at `tests/brainstorm-server/` - verifies server behavior +4. **Brainstorming skill** updated with visual companion section and `visual-companion.md` reference doc + +**To use:** +1. Start server as background job: `node lib/brainstorm-server/index.js &` +2. Tell user to open `http://localhost:3333` +3. Write HTML to `/tmp/brainstorm/screen.html` +4. Check task output for user events From 94d5f4a817959db0ec93ad43bd20cfbe88c5a784 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 16:38:21 -0800 Subject: [PATCH 09/19] feat: add sendToClaude helper and wait-for-event tool - Add sendToClaude() function to browser helper that shows confirmation - Add wait-for-event.sh script for watching server output (tail -f | grep -m 1) - Enables clean event-driven loop: background bash waits for event, completion triggers Claude's turn --- lib/brainstorm-server/helper.js | 20 +++++++++++++++++++- lib/brainstorm-server/wait-for-event.sh | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100755 lib/brainstorm-server/wait-for-event.sh diff --git a/lib/brainstorm-server/helper.js b/lib/brainstorm-server/helper.js index 77ca052be..170b35ccf 100644 --- a/lib/brainstorm-server/helper.js +++ b/lib/brainstorm-server/helper.js @@ -87,10 +87,28 @@ }, 500); // 500ms debounce }); + // Send to Claude - triggers server to exit and return all events + function sendToClaude(feedback) { + send({ + type: 'send-to-claude', + feedback: feedback || null + }); + // Show confirmation to user + document.body.innerHTML = ` +
+
+

✓ Sent to Claude

+

Return to the terminal to see Claude's response.

+
+
+ `; + } + // Expose for explicit use if needed window.brainstorm = { send: send, - choice: (value, metadata = {}) => send({ type: 'choice', value, ...metadata }) + choice: (value, metadata = {}) => send({ type: 'choice', value, ...metadata }), + sendToClaude: sendToClaude }; connect(); diff --git a/lib/brainstorm-server/wait-for-event.sh b/lib/brainstorm-server/wait-for-event.sh new file mode 100755 index 000000000..91593e387 --- /dev/null +++ b/lib/brainstorm-server/wait-for-event.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Wait for a browser event from the brainstorm server +# Usage: wait-for-event.sh [event-type] +# +# Blocks until a matching event arrives, then prints it and exits. +# Default event type: "send-to-claude" + +OUTPUT_FILE="${1:?Usage: wait-for-event.sh [event-type]}" +EVENT_TYPE="${2:-send-to-claude}" + +if [[ ! -f "$OUTPUT_FILE" ]]; then + echo "Error: Output file not found: $OUTPUT_FILE" >&2 + exit 1 +fi + +# Wait for new lines matching the event type +# -n 0: start at end (only new content) +# -f: follow +# grep -m 1: exit after first match +tail -n 0 -f "$OUTPUT_FILE" | grep -m 1 "$EVENT_TYPE" From 2a61167b02669d0c8c1acc2b083e48c0d79b2532 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 16:47:03 -0800 Subject: [PATCH 10/19] feat: add visual companion for brainstorming skill Adds browser-based mockup display to replace ASCII art during brainstorming sessions. Key components: - Frame template with OS-aware light/dark theming - CSS helpers for options, cards, mockups, split views - Server lifecycle scripts (start/stop with random high port) - Event watcher using tail+grep for feedback loop - Claude instructions for using the visual companion The skill now asks users if they want browser mockups and only runs in Claude Code environments. --- lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md | 199 ++++++++++++++ lib/brainstorm-server/frame-template.html | 259 +++++++++++++++++++ lib/brainstorm-server/index.js | 3 +- lib/brainstorm-server/start-server.sh | 42 +++ lib/brainstorm-server/stop-server.sh | 15 ++ skills/brainstorming/SKILL.md | 78 +++++- skills/brainstorming/visual-companion.md | 121 ++++++--- 7 files changed, 659 insertions(+), 58 deletions(-) create mode 100644 lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md create mode 100644 lib/brainstorm-server/frame-template.html create mode 100755 lib/brainstorm-server/start-server.sh create mode 100755 lib/brainstorm-server/stop-server.sh diff --git a/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md b/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md new file mode 100644 index 000000000..babb86134 --- /dev/null +++ b/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md @@ -0,0 +1,199 @@ +# Visual Companion Instructions for Claude + +This document explains how to use the brainstorm visual companion to show mockups, designs, and options to users without resorting to ASCII art. + +## When to Use + +Use the visual companion when you need to show: +- **UI mockups** - layouts, navigation patterns, component designs +- **Design comparisons** - "Which of these 3 approaches works better?" +- **Interactive prototypes** - clickable wireframes +- **Visual choices** - anything where seeing beats describing + +**Don't use it for:** simple text questions, code review, or when the user prefers terminal-only interaction. + +## Lifecycle + +```bash +# Start server (returns JSON with URL) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh + +# Tell user to open the URL in their browser + +# Write screens to /tmp/brainstorm/screen.html (auto-refreshes) + +# Wait for user feedback +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh /path/to/server.log + +# When done, stop server +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh +``` + +## Writing Screens + +Copy the frame template structure but replace `#claude-content` with your content: + +```html +
+

Your Question

+

Brief context

+ + +
+``` + +The frame template (`frame-template.html`) includes CSS for: +- OS-aware light/dark theming +- Fixed header and feedback footer +- Common UI patterns (see below) + +## CSS Helper Classes + +### Options (A/B/C choices) + +```html +
+
+
A
+
+

Option Title

+

Description of this option

+
+
+ +
+``` + +### Cards (visual designs) + +```html +
+
+
+ +
+
+

Design Name

+

Brief description

+
+
+
+``` + +### Mockup Container + +```html +
+
Preview: Dashboard Layout
+
+ +
+
+``` + +### Split View (side-by-side) + +```html +
+
+
+
+``` + +### Pros/Cons + +```html +
+
+

Pros

+
    +
  • Benefit one
  • +
  • Benefit two
  • +
+
+
+

Cons

+
    +
  • Drawback one
  • +
  • Drawback two
  • +
+
+
+``` + +### Inline Mockup Elements + +```html +
Logo | Home | About | Contact
+
+
Navigation
+
Main content area
+
+ + +``` + +## User Feedback + +When the user clicks Send, you receive JSON like: + +```json +{"choice": "a", "feedback": "I like this but make the header smaller"} +``` + +- `choice` - which option/card they selected (from `data-choice` attribute) +- `feedback` - any notes they typed + +## Example: Design Comparison + +```html +
+

Which blog layout works better?

+

Consider readability and visual hierarchy

+ +
+
+
+
+
Blog Title
+
+

Post Title

+

+ Content preview text goes here... +

+
+
+
+
+

Classic Layout

+

Traditional blog with posts in a single column

+
+
+ +
+
+
+
Blog Title
+
+
Featured
+
Post
+
+
+
+
+

Magazine Layout

+

Grid-based with featured posts

+
+
+
+
+``` + +## Tips + +1. **Keep mockups simple** - Focus on layout and structure, not pixel-perfect design +2. **Use placeholders** - The `.placeholder` class works great for content areas +3. **Label clearly** - Use `.mockup-header` to explain what each mockup shows +4. **Limit choices** - 2-4 options is ideal; more gets overwhelming +5. **Provide context** - Use `.subtitle` to explain what you're asking +6. **Regenerate fully** - Write the complete HTML each turn; don't try to patch diff --git a/lib/brainstorm-server/frame-template.html b/lib/brainstorm-server/frame-template.html new file mode 100644 index 000000000..f169b8e3d --- /dev/null +++ b/lib/brainstorm-server/frame-template.html @@ -0,0 +1,259 @@ + + + + Brainstorm Companion + + + +
+

Brainstorm Companion

+
Connected
+
+ +
+
+ +

Visual Brainstorming

+

Claude will show mockups and options here.

+
+
+ + + + + + diff --git a/lib/brainstorm-server/index.js b/lib/brainstorm-server/index.js index 8468ad5ea..d433fdf00 100644 --- a/lib/brainstorm-server/index.js +++ b/lib/brainstorm-server/index.js @@ -5,7 +5,8 @@ const chokidar = require('chokidar'); const fs = require('fs'); const path = require('path'); -const PORT = process.env.BRAINSTORM_PORT || 3333; +// Use provided port or pick a random high port (49152-65535) +const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383)); const SCREEN_FILE = process.env.BRAINSTORM_SCREEN || '/tmp/brainstorm/screen.html'; const SCREEN_DIR = path.dirname(SCREEN_FILE); diff --git a/lib/brainstorm-server/start-server.sh b/lib/brainstorm-server/start-server.sh new file mode 100755 index 000000000..2bb57c69f --- /dev/null +++ b/lib/brainstorm-server/start-server.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Start the brainstorm server and output connection info +# Usage: start-server.sh +# +# Starts server on a random high port, outputs JSON with URL +# Server runs in background, PID saved for cleanup + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SCREEN_DIR="${BRAINSTORM_SCREEN_DIR:-/tmp/brainstorm}" +SCREEN_FILE="${SCREEN_DIR}/screen.html" +PID_FILE="${SCREEN_DIR}/.server.pid" +LOG_FILE="${SCREEN_DIR}/.server.log" + +# Ensure screen directory exists +mkdir -p "$SCREEN_DIR" + +# Kill any existing server +if [[ -f "$PID_FILE" ]]; then + old_pid=$(cat "$PID_FILE") + kill "$old_pid" 2>/dev/null + rm -f "$PID_FILE" +fi + +# Start server, capturing output to log file +cd "$SCRIPT_DIR" +node index.js > "$LOG_FILE" 2>&1 & +SERVER_PID=$! +echo "$SERVER_PID" > "$PID_FILE" + +# Wait for server-started message (check log file) +for i in {1..50}; do + if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then + # Extract and output the server-started line + grep "server-started" "$LOG_FILE" | head -1 + exit 0 + fi + sleep 0.1 +done + +# Timeout - server didn't start +echo '{"error": "Server failed to start within 5 seconds"}' +exit 1 diff --git a/lib/brainstorm-server/stop-server.sh b/lib/brainstorm-server/stop-server.sh new file mode 100755 index 000000000..87ec68aea --- /dev/null +++ b/lib/brainstorm-server/stop-server.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Stop the brainstorm server and clean up +# Usage: stop-server.sh + +SCREEN_DIR="${BRAINSTORM_SCREEN_DIR:-/tmp/brainstorm}" +PID_FILE="${SCREEN_DIR}/.server.pid" + +if [[ -f "$PID_FILE" ]]; then + pid=$(cat "$PID_FILE") + kill "$pid" 2>/dev/null + rm -f "$PID_FILE" + echo '{"status": "stopped"}' +else + echo '{"status": "not_running"}' +fi diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 717e3c2dc..77ee8fe05 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -53,22 +53,72 @@ Start by understanding the current project context, then ask questions one at a - **Incremental validation** - Present design in sections, validate each - **Be flexible** - Go back and clarify when something doesn't make sense -## Visual Companion (Optional) +## Visual Companion (Claude Code Only) -When brainstorming involves visual elements - UI mockups, wireframes, interactive prototypes - use the browser-based visual companion. +When brainstorming involves visual elements - UI mockups, layouts, design comparisons - you can use a browser-based visual companion instead of ASCII art. **This only works in Claude Code.** -**When to use:** -- Presenting UI/UX options that benefit from visual comparison -- Showing wireframes or layout options -- Gathering structured feedback (ratings, forms) -- Prototyping click interactions +### When to Offer -**How it works:** -1. Start the server as a background job -2. Tell user to open http://localhost:3333 -3. Write HTML to `/tmp/brainstorm/screen.html` (auto-refreshes) -4. Check background task output for user interactions +If the brainstorm involves visual decisions (UI layouts, design choices, mockups), ask the user: -The terminal remains the primary conversation interface. The browser is a visual aid. +> "This involves some visual decisions. Would you like me to show mockups in a browser window? (Requires opening a local URL)" -**Reference:** See `visual-companion.md` in this skill directory for HTML patterns and API details. +Only proceed with visual companion if they agree. Otherwise, describe options in text. + +### Starting the Visual Companion + +```bash +# Start server (outputs JSON with URL) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh + +# Output looks like: {"type":"server-started","port":52341,"url":"http://localhost:52341"} +``` + +Tell the user to open the URL in their browser. + +### Showing Content + +Write complete HTML to `/tmp/brainstorm/screen.html`. The browser auto-refreshes. + +Use the frame template structure from `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`: +- Keep the header and feedback-footer intact +- Replace `#claude-content` with your content +- Use the CSS helper classes (`.options`, `.cards`, `.mockup`, `.split`, `.pros-cons`) + +See `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` for detailed examples. + +### Waiting for User Feedback + +Run the watcher as a background bash command: + +```bash +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh /tmp/brainstorm/.server.log +``` + +When the user clicks Send in the browser, the watcher exits and you receive their feedback as JSON: +```json +{"choice": "a", "feedback": "I like this but make the header smaller"} +``` + +### The Loop + +1. Write screen HTML +2. Start watcher (background bash) +3. Watcher completes when user sends feedback +4. Read feedback, respond with new screen +5. Repeat until done + +### Cleaning Up + +When the visual brainstorming session is complete: + +```bash +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh +``` + +### Tips + +- **Keep mockups simple** - Focus on layout and structure, not pixel-perfect design +- **Limit choices** - 2-4 options is ideal +- **Regenerate fully** - Write complete HTML each turn; the screen is stateless +- **Terminal is primary** - The browser shows things; conversation happens in terminal diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index a4a9f9071..7cc388399 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -1,73 +1,108 @@ # Visual Companion Reference -## Starting the Server +Quick reference for using the visual brainstorming companion. -Run as a background job: +## Files -```bash -node ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/index.js -``` +| File | Purpose | +|------|---------| +| `lib/brainstorm-server/start-server.sh` | Start server, outputs JSON with URL | +| `lib/brainstorm-server/stop-server.sh` | Stop server and clean up | +| `lib/brainstorm-server/wait-for-event.sh` | Wait for user feedback | +| `lib/brainstorm-server/frame-template.html` | Base HTML template with CSS | +| `lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` | Detailed usage guide | +| `/tmp/brainstorm/screen.html` | Write your screens here | +| `/tmp/brainstorm/.server.log` | Server output (for watcher) | -Tell the user: "I've started a visual companion at http://localhost:3333 - open it in a browser." +## Quick Start -## Pushing Screens +```bash +# 1. Start server +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh +# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341"} -Write HTML to `/tmp/brainstorm/screen.html`. The server watches this file and auto-refreshes the browser. +# 2. Write screen +# Write HTML to /tmp/brainstorm/screen.html -## Reading User Responses +# 3. Wait for feedback (background) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh /tmp/brainstorm/.server.log -Check the background task output for JSON events: +# 4. Read watcher output when it completes +# Returns: {"choice":"a","feedback":"user notes"} -```json -{"source":"user-event","type":"click","text":"Option A","choice":"optionA","timestamp":1234567890} -{"source":"user-event","type":"submit","data":{"notes":"My feedback"},"timestamp":1234567891} +# 5. Clean up when done +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh ``` -Event types: -- **click**: User clicked button or `data-choice` element -- **submit**: User submitted form (includes all form data) -- **input**: User typed in field (debounced 500ms) -- **choice**: Explicit choice via `brainstorm.choice()` call - -## HTML Patterns - -### Choice Cards +## CSS Classes +### Options (A/B/C choices) ```html
- - +
+
A
+
+

Title

+

Description

+
+
``` -### Interactive Mockup +### Cards (visual designs) +```html +
+
+
+
+

Name

+

Description

+
+
+
+``` +### Mockup container ```html
-
App Header
- -
Content
+
Label
+
``` -### Form with Notes - +### Split view ```html -
- - - -
+
+
+
+
``` -### Explicit JavaScript +### Pros/Cons +```html +
+

Pros

  • ...
+

Cons

  • ...
+
+``` +### Mock elements ```html - +
Nav items
+
Sidebar
+
Content
+ + +
Placeholder area
``` + +## User Feedback Format + +```json +{ + "choice": "option-id", // from data-choice attribute + "feedback": "user notes" // from feedback textarea +} +``` + +Both fields are optional - user may select without notes, or send notes without selection. From b98afbd74fc346b3fa293d9e638716d3823dc1c1 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 17:25:45 -0800 Subject: [PATCH 11/19] fix: session isolation and blocking wait for visual companion - Each session gets unique temp directory (/tmp/brainstorm-{pid}-{timestamp}) - Server outputs screen_dir and screen_file in startup JSON - stop-server.sh takes screen_dir arg and cleans up session directory - Document blocking TaskOutput pattern: 10-min timeouts, retry up to 3x, then prompt user "let me know when you want to continue" --- lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md | 19 +++++++---- lib/brainstorm-server/index.js | 8 ++++- lib/brainstorm-server/start-server.sh | 10 ++++-- lib/brainstorm-server/stop-server.sh | 15 ++++++--- skills/brainstorming/SKILL.md | 35 +++++++++++++------- skills/brainstorming/visual-companion.md | 34 ++++++++++--------- 6 files changed, 79 insertions(+), 42 deletions(-) diff --git a/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md b/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md index babb86134..b69d8fa13 100644 --- a/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md +++ b/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md @@ -15,18 +15,25 @@ Use the visual companion when you need to show: ## Lifecycle ```bash -# Start server (returns JSON with URL) +# Start server (returns JSON with URL and session paths) ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh +# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341", +# "screen_dir":"/tmp/brainstorm-12345-1234567890", +# "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"} + +# Save screen_dir and screen_file from response! # Tell user to open the URL in their browser -# Write screens to /tmp/brainstorm/screen.html (auto-refreshes) +# Write screens to screen_file (auto-refreshes) -# Wait for user feedback -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh /path/to/server.log +# Wait for user feedback: +# 1. Start watcher in background +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log +# 2. Immediately call TaskOutput(task_id, block=true) to wait for completion -# When done, stop server -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh +# When done, stop server (pass screen_dir) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR ``` ## Writing Screens diff --git a/lib/brainstorm-server/index.js b/lib/brainstorm-server/index.js index d433fdf00..21390be73 100644 --- a/lib/brainstorm-server/index.js +++ b/lib/brainstorm-server/index.js @@ -81,5 +81,11 @@ chokidar.watch(SCREEN_FILE).on('change', () => { }); server.listen(PORT, '127.0.0.1', () => { - console.log(JSON.stringify({ type: 'server-started', port: PORT, url: `http://localhost:${PORT}` })); + console.log(JSON.stringify({ + type: 'server-started', + port: PORT, + url: `http://localhost:${PORT}`, + screen_dir: SCREEN_DIR, + screen_file: SCREEN_FILE + })); }); diff --git a/lib/brainstorm-server/start-server.sh b/lib/brainstorm-server/start-server.sh index 2bb57c69f..1b9992c91 100755 --- a/lib/brainstorm-server/start-server.sh +++ b/lib/brainstorm-server/start-server.sh @@ -3,15 +3,19 @@ # Usage: start-server.sh # # Starts server on a random high port, outputs JSON with URL +# Each session gets its own temp directory to avoid conflicts # Server runs in background, PID saved for cleanup SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SCREEN_DIR="${BRAINSTORM_SCREEN_DIR:-/tmp/brainstorm}" + +# Generate unique session directory +SESSION_ID="$$-$(date +%s)" +SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}" SCREEN_FILE="${SCREEN_DIR}/screen.html" PID_FILE="${SCREEN_DIR}/.server.pid" LOG_FILE="${SCREEN_DIR}/.server.log" -# Ensure screen directory exists +# Create fresh session directory mkdir -p "$SCREEN_DIR" # Kill any existing server @@ -23,7 +27,7 @@ fi # Start server, capturing output to log file cd "$SCRIPT_DIR" -node index.js > "$LOG_FILE" 2>&1 & +BRAINSTORM_SCREEN="$SCREEN_FILE" node index.js > "$LOG_FILE" 2>&1 & SERVER_PID=$! echo "$SERVER_PID" > "$PID_FILE" diff --git a/lib/brainstorm-server/stop-server.sh b/lib/brainstorm-server/stop-server.sh index 87ec68aea..cb57d8c41 100755 --- a/lib/brainstorm-server/stop-server.sh +++ b/lib/brainstorm-server/stop-server.sh @@ -1,14 +1,21 @@ #!/bin/bash -# Stop the brainstorm server and clean up -# Usage: stop-server.sh +# Stop the brainstorm server and clean up session directory +# Usage: stop-server.sh + +SCREEN_DIR="$1" + +if [[ -z "$SCREEN_DIR" ]]; then + echo '{"error": "Usage: stop-server.sh "}' + exit 1 +fi -SCREEN_DIR="${BRAINSTORM_SCREEN_DIR:-/tmp/brainstorm}" PID_FILE="${SCREEN_DIR}/.server.pid" if [[ -f "$PID_FILE" ]]; then pid=$(cat "$PID_FILE") kill "$pid" 2>/dev/null - rm -f "$PID_FILE" + # Clean up session directory + rm -rf "$SCREEN_DIR" echo '{"status": "stopped"}' else echo '{"status": "not_running"}' diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 77ee8fe05..c5d0e366c 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -68,17 +68,22 @@ Only proceed with visual companion if they agree. Otherwise, describe options in ### Starting the Visual Companion ```bash -# Start server (outputs JSON with URL) +# Start server (creates unique session directory) ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh -# Output looks like: {"type":"server-started","port":52341,"url":"http://localhost:52341"} +# Output looks like: +# {"type":"server-started","port":52341,"url":"http://localhost:52341", +# "screen_dir":"/tmp/brainstorm-12345-1234567890", +# "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"} ``` +**Save the `screen_dir` and `screen_file` paths from the response** - you'll need them throughout the session. + Tell the user to open the URL in their browser. ### Showing Content -Write complete HTML to `/tmp/brainstorm/screen.html`. The browser auto-refreshes. +Write complete HTML to the session's `screen_file` path. The browser auto-refreshes. Use the frame template structure from `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`: - Keep the header and feedback-footer intact @@ -89,31 +94,37 @@ See `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` for det ### Waiting for User Feedback -Run the watcher as a background bash command: +Start the watcher as a background bash command, then use TaskOutput with block=true to wait: ```bash -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh /tmp/brainstorm/.server.log +# 1. Start watcher in background +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log + +# 2. Call TaskOutput(task_id, block=true, timeout=600000) to wait +# 3. If timeout, call TaskOutput again (watcher is still running) +# 4. After 3 timeouts (30 min), say "Let me know when you want to continue" and stop looping ``` -When the user clicks Send in the browser, the watcher exits and you receive their feedback as JSON: +When the user clicks Send in the browser, the watcher exits and TaskOutput returns with feedback: ```json {"choice": "a", "feedback": "I like this but make the header smaller"} ``` ### The Loop -1. Write screen HTML +1. Write screen HTML to `screen_file` 2. Start watcher (background bash) -3. Watcher completes when user sends feedback -4. Read feedback, respond with new screen -5. Repeat until done +3. Call TaskOutput(task_id, block=true) to wait +4. TaskOutput returns with feedback +5. Respond with new screen +6. Repeat until done ### Cleaning Up -When the visual brainstorming session is complete: +When the visual brainstorming session is complete, pass the screen_dir to stop: ```bash -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR ``` ### Tips diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 7cc388399..c6a2756eb 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -6,32 +6,34 @@ Quick reference for using the visual brainstorming companion. | File | Purpose | |------|---------| -| `lib/brainstorm-server/start-server.sh` | Start server, outputs JSON with URL | -| `lib/brainstorm-server/stop-server.sh` | Stop server and clean up | +| `lib/brainstorm-server/start-server.sh` | Start server, outputs JSON with URL and session paths | +| `lib/brainstorm-server/stop-server.sh` | Stop server and clean up session directory | | `lib/brainstorm-server/wait-for-event.sh` | Wait for user feedback | | `lib/brainstorm-server/frame-template.html` | Base HTML template with CSS | | `lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` | Detailed usage guide | -| `/tmp/brainstorm/screen.html` | Write your screens here | -| `/tmp/brainstorm/.server.log` | Server output (for watcher) | ## Quick Start ```bash -# 1. Start server +# 1. Start server (creates unique session directory) ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh -# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341"} - -# 2. Write screen -# Write HTML to /tmp/brainstorm/screen.html - -# 3. Wait for feedback (background) -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh /tmp/brainstorm/.server.log - -# 4. Read watcher output when it completes +# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341", +# "screen_dir":"/tmp/brainstorm-12345-1234567890", +# "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"} + +# 2. Write screen to the session's screen_file +# Use Bash with heredoc to write HTML + +# 3. Wait for feedback using TaskOutput with block=true +# Start watcher in background: +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log +# Then call TaskOutput(task_id, block=true, timeout=600000) to wait +# If timeout, call TaskOutput again (watcher still running) +# After 3 timeouts (30 min), say "Let me know when you want to continue" # Returns: {"choice":"a","feedback":"user notes"} -# 5. Clean up when done -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh +# 4. Clean up when done (pass screen_dir as argument) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR ``` ## CSS Classes From 70c2d06c4e792d0c5e4c896e70765ddf2a5affa7 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 18:35:37 -0800 Subject: [PATCH 12/19] feat: add show-and-wait.sh helper, fix race condition - New show-and-wait.sh combines write + wait into one command - Uses polling instead of tail -f (which hangs on macOS) - Docs updated: start watcher BEFORE writing screen to avoid race - Reduces terminal noise by consolidating operations --- lib/brainstorm-server/show-and-wait.sh | 32 ++++++++++++++++++++++++ skills/brainstorming/SKILL.md | 6 ++--- skills/brainstorming/visual-companion.md | 12 ++++----- 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100755 lib/brainstorm-server/show-and-wait.sh diff --git a/lib/brainstorm-server/show-and-wait.sh b/lib/brainstorm-server/show-and-wait.sh new file mode 100755 index 000000000..02519f0e1 --- /dev/null +++ b/lib/brainstorm-server/show-and-wait.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Write HTML to screen and wait for user feedback +# Usage: show-and-wait.sh < html_content +# +# Reads HTML from stdin, writes to screen_file, waits for feedback. +# Outputs the feedback JSON when user sends from browser. + +SCREEN_DIR="${1:?Usage: show-and-wait.sh }" +SCREEN_FILE="${SCREEN_DIR}/screen.html" +LOG_FILE="${SCREEN_DIR}/.server.log" + +if [[ ! -d "$SCREEN_DIR" ]]; then + echo '{"error": "Screen directory not found"}' >&2 + exit 1 +fi + +# Write HTML from stdin to screen file +cat > "$SCREEN_FILE" + +# Record current position in log file +LOG_POS=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0) + +# Poll for new lines containing the event +while true; do + # Check for new matching lines since our starting position + RESULT=$(tail -n +$((LOG_POS + 1)) "$LOG_FILE" 2>/dev/null | grep -m 1 "send-to-claude") + if [[ -n "$RESULT" ]]; then + echo "$RESULT" + exit 0 + fi + sleep 0.2 +done diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index c5d0e366c..2659dda17 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -112,9 +112,9 @@ When the user clicks Send in the browser, the watcher exits and TaskOutput retur ### The Loop -1. Write screen HTML to `screen_file` -2. Start watcher (background bash) -3. Call TaskOutput(task_id, block=true) to wait +1. Start watcher (background bash) - must be FIRST to avoid race condition +2. Write screen HTML to `screen_file` +3. Call TaskOutput(task_id, block=true, timeout=600000) to wait 4. TaskOutput returns with feedback 5. Respond with new screen 6. Repeat until done diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index c6a2756eb..4ca126042 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -21,18 +21,18 @@ ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh # "screen_dir":"/tmp/brainstorm-12345-1234567890", # "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"} -# 2. Write screen to the session's screen_file +# 2. Start watcher FIRST (before writing screen - avoids race condition) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log + +# 3. Write screen to the session's screen_file # Use Bash with heredoc to write HTML -# 3. Wait for feedback using TaskOutput with block=true -# Start watcher in background: -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log -# Then call TaskOutput(task_id, block=true, timeout=600000) to wait +# 4. Wait for feedback - call TaskOutput(task_id, block=true, timeout=600000) # If timeout, call TaskOutput again (watcher still running) # After 3 timeouts (30 min), say "Let me know when you want to continue" # Returns: {"choice":"a","feedback":"user notes"} -# 4. Clean up when done (pass screen_dir as argument) +# 5. Clean up when done (pass screen_dir as argument) ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR ``` From de2e15242c5bc36c97a36ca3e895db6902986586 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 18:44:50 -0800 Subject: [PATCH 13/19] refactor: simplify visual companion workflow, improve guidance Scripts: - Rename show-and-wait.sh -> wait-for-feedback.sh (just waits, no HTML piping) - Remove wait-for-event.sh (used hanging tail -f) - Workflow now: Write tool for HTML, wait-for-feedback.sh to block Documentation rewrite: - Broader "when to use" (UI, architecture, complex choices, spatial) - Always ask user first before starting - Scale fidelity to the question being asked - Explain the question on each page - Iterate before moving on - validate changes address feedback - Use real content (Unsplash images) when it matters --- lib/brainstorm-server/wait-for-event.sh | 20 ---- ...{show-and-wait.sh => wait-for-feedback.sh} | 15 +-- skills/brainstorming/SKILL.md | 91 +++++++++---------- skills/brainstorming/visual-companion.md | 34 ++++--- 4 files changed, 67 insertions(+), 93 deletions(-) delete mode 100755 lib/brainstorm-server/wait-for-event.sh rename lib/brainstorm-server/{show-and-wait.sh => wait-for-feedback.sh} (51%) diff --git a/lib/brainstorm-server/wait-for-event.sh b/lib/brainstorm-server/wait-for-event.sh deleted file mode 100755 index 91593e387..000000000 --- a/lib/brainstorm-server/wait-for-event.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# Wait for a browser event from the brainstorm server -# Usage: wait-for-event.sh [event-type] -# -# Blocks until a matching event arrives, then prints it and exits. -# Default event type: "send-to-claude" - -OUTPUT_FILE="${1:?Usage: wait-for-event.sh [event-type]}" -EVENT_TYPE="${2:-send-to-claude}" - -if [[ ! -f "$OUTPUT_FILE" ]]; then - echo "Error: Output file not found: $OUTPUT_FILE" >&2 - exit 1 -fi - -# Wait for new lines matching the event type -# -n 0: start at end (only new content) -# -f: follow -# grep -m 1: exit after first match -tail -n 0 -f "$OUTPUT_FILE" | grep -m 1 "$EVENT_TYPE" diff --git a/lib/brainstorm-server/show-and-wait.sh b/lib/brainstorm-server/wait-for-feedback.sh similarity index 51% rename from lib/brainstorm-server/show-and-wait.sh rename to lib/brainstorm-server/wait-for-feedback.sh index 02519f0e1..fb4316bdd 100755 --- a/lib/brainstorm-server/show-and-wait.sh +++ b/lib/brainstorm-server/wait-for-feedback.sh @@ -1,12 +1,11 @@ #!/bin/bash -# Write HTML to screen and wait for user feedback -# Usage: show-and-wait.sh < html_content +# Wait for user feedback from the brainstorm browser +# Usage: wait-for-feedback.sh # -# Reads HTML from stdin, writes to screen_file, waits for feedback. -# Outputs the feedback JSON when user sends from browser. +# Blocks until user sends feedback, then outputs the JSON. +# Write HTML to screen_file BEFORE calling this. -SCREEN_DIR="${1:?Usage: show-and-wait.sh }" -SCREEN_FILE="${SCREEN_DIR}/screen.html" +SCREEN_DIR="${1:?Usage: wait-for-feedback.sh }" LOG_FILE="${SCREEN_DIR}/.server.log" if [[ ! -d "$SCREEN_DIR" ]]; then @@ -14,15 +13,11 @@ if [[ ! -d "$SCREEN_DIR" ]]; then exit 1 fi -# Write HTML from stdin to screen file -cat > "$SCREEN_FILE" - # Record current position in log file LOG_POS=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0) # Poll for new lines containing the event while true; do - # Check for new matching lines since our starting position RESULT=$(tail -n +$((LOG_POS + 1)) "$LOG_FILE" 2>/dev/null | grep -m 1 "send-to-claude") if [[ -n "$RESULT" ]]; then echo "$RESULT" diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 2659dda17..9eb21cab1 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -55,81 +55,74 @@ Start by understanding the current project context, then ask questions one at a ## Visual Companion (Claude Code Only) -When brainstorming involves visual elements - UI mockups, layouts, design comparisons - you can use a browser-based visual companion instead of ASCII art. **This only works in Claude Code.** +A browser-based visual companion for showing mockups, diagrams, and options. Use it whenever visual representation makes feedback easier. **Only works in Claude Code.** -### When to Offer +### When to Use -If the brainstorm involves visual decisions (UI layouts, design choices, mockups), ask the user: +Use the visual companion when seeing beats describing: +- **UI mockups** - layouts, navigation, component designs +- **Architecture diagrams** - system components, data flow, relationships +- **Complex choices** - multi-option decisions with visual trade-offs +- **Design polish** - when the question is about look and feel +- **Spatial relationships** - file structures, database schemas, state machines +**Always ask first:** > "This involves some visual decisions. Would you like me to show mockups in a browser window? (Requires opening a local URL)" -Only proceed with visual companion if they agree. Otherwise, describe options in text. +Only proceed if they agree. Otherwise, describe options in text. -### Starting the Visual Companion +### How to Use Effectively -```bash -# Start server (creates unique session directory) -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh +**Scale fidelity to the question.** If you're asking about layout structure, simple wireframes suffice. If you're asking about visual polish, show polish. Match the mockup's detail level to what you're trying to learn. -# Output looks like: -# {"type":"server-started","port":52341,"url":"http://localhost:52341", -# "screen_dir":"/tmp/brainstorm-12345-1234567890", -# "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"} -``` +**Explain the question on each page.** Don't just show options—state what decision you're seeking. "Which layout feels more professional?" not just "Pick one." -**Save the `screen_dir` and `screen_file` paths from the response** - you'll need them throughout the session. +**Iterate before moving on.** If feedback changes the current screen, update it and show again. Validate that your changes address their feedback before proceeding to the next question. -Tell the user to open the URL in their browser. +**Limit choices to 2-4 options.** More gets overwhelming. If you have more alternatives, narrow them down first or group them. -### Showing Content +**Use real content when it matters.** For a photography portfolio, use actual images (Unsplash). For a blog, use realistic text. Placeholder content obscures design issues. -Write complete HTML to the session's `screen_file` path. The browser auto-refreshes. +### Starting a Session -Use the frame template structure from `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`: -- Keep the header and feedback-footer intact -- Replace `#claude-content` with your content -- Use the CSS helper classes (`.options`, `.cards`, `.mockup`, `.split`, `.pros-cons`) +```bash +# Start server (creates unique session directory) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh -See `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` for detailed examples. +# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341", +# "screen_dir":"/tmp/brainstorm-12345","screen_file":"/tmp/brainstorm-12345/screen.html"} +``` -### Waiting for User Feedback +Save `screen_dir` and `screen_file` from the response. Tell user to open the URL. -Start the watcher as a background bash command, then use TaskOutput with block=true to wait: +### The Loop -```bash -# 1. Start watcher in background -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log +1. **Start watcher first** (background bash) - avoids race condition: + ```bash + ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR + ``` -# 2. Call TaskOutput(task_id, block=true, timeout=600000) to wait -# 3. If timeout, call TaskOutput again (watcher is still running) -# 4. After 3 timeouts (30 min), say "Let me know when you want to continue" and stop looping -``` +2. **Write HTML** to `screen_file` using the Write tool (browser auto-refreshes) -When the user clicks Send in the browser, the watcher exits and TaskOutput returns with feedback: -```json -{"choice": "a", "feedback": "I like this but make the header smaller"} -``` +3. **Wait for feedback** - call `TaskOutput(task_id, block=true, timeout=600000)` + - If timeout, call TaskOutput again (watcher still running) + - After 3 timeouts (30 min), say "Let me know when you want to continue" -### The Loop +4. **Process feedback** - returns JSON like `{"choice": "a", "feedback": "make header smaller"}` -1. Start watcher (background bash) - must be FIRST to avoid race condition -2. Write screen HTML to `screen_file` -3. Call TaskOutput(task_id, block=true, timeout=600000) to wait -4. TaskOutput returns with feedback -5. Respond with new screen -6. Repeat until done +5. **Iterate or advance** - if feedback changes current screen, update and re-show. Only move to next question when current step is validated. -### Cleaning Up +6. Repeat until done. -When the visual brainstorming session is complete, pass the screen_dir to stop: +### Cleaning Up ```bash ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR ``` -### Tips +### Resources -- **Keep mockups simple** - Focus on layout and structure, not pixel-perfect design -- **Limit choices** - 2-4 options is ideal -- **Regenerate fully** - Write complete HTML each turn; the screen is stateless -- **Terminal is primary** - The browser shows things; conversation happens in terminal +- Frame template: `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html` +- CSS classes: `.options`, `.cards`, `.mockup`, `.split`, `.pros-cons` +- Detailed examples: `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` +- Quick reference: `${CLAUDE_PLUGIN_ROOT}/skills/brainstorming/visual-companion.md` diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 4ca126042..37d4f07a0 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -1,6 +1,6 @@ # Visual Companion Reference -Quick reference for using the visual brainstorming companion. +Quick reference for the browser-based visual brainstorming companion. ## Files @@ -8,34 +8,40 @@ Quick reference for using the visual brainstorming companion. |------|---------| | `lib/brainstorm-server/start-server.sh` | Start server, outputs JSON with URL and session paths | | `lib/brainstorm-server/stop-server.sh` | Stop server and clean up session directory | -| `lib/brainstorm-server/wait-for-event.sh` | Wait for user feedback | +| `lib/brainstorm-server/wait-for-feedback.sh` | Wait for user feedback (polling-based) | | `lib/brainstorm-server/frame-template.html` | Base HTML template with CSS | | `lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` | Detailed usage guide | ## Quick Start ```bash -# 1. Start server (creates unique session directory) +# 1. Start server ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh -# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341", -# "screen_dir":"/tmp/brainstorm-12345-1234567890", -# "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"} +# Returns: {"screen_dir":"/tmp/brainstorm-xxx","screen_file":"...","url":"http://localhost:PORT"} -# 2. Start watcher FIRST (before writing screen - avoids race condition) -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log +# 2. Start watcher FIRST (background bash) - avoids race condition +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR -# 3. Write screen to the session's screen_file -# Use Bash with heredoc to write HTML +# 3. Write HTML to screen_file using Write tool (browser auto-refreshes) -# 4. Wait for feedback - call TaskOutput(task_id, block=true, timeout=600000) -# If timeout, call TaskOutput again (watcher still running) -# After 3 timeouts (30 min), say "Let me know when you want to continue" +# 4. Call TaskOutput(task_id, block=true, timeout=600000) +# If timeout, call again. After 3 timeouts (30 min), prompt user. # Returns: {"choice":"a","feedback":"user notes"} -# 5. Clean up when done (pass screen_dir as argument) +# 5. Iterate or advance - update screen if feedback changes it, else next question + +# 6. Clean up when done ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR ``` +## Key Principles + +- **Always ask first** before starting visual companion +- **Scale fidelity to the question** - wireframes for layout, polish for polish questions +- **Explain the question** on each page - what decision are you seeking? +- **Iterate before advancing** - if feedback changes current screen, update and re-show +- **2-4 options max** per screen + ## CSS Classes ### Options (A/B/C choices) From e9263c9754965e2dd06456e9a7da34bf569beb1c Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 18:55:05 -0800 Subject: [PATCH 14/19] docs: improve terminal UX for visual companion - Never use cat/heredoc for HTML (dumps noise into terminal) - Read screen_file first before Write tool to avoid errors - Remind user of URL on every step, not just first - Give text summary of what's on screen before they look --- skills/brainstorming/SKILL.md | 18 +++++++++++++----- skills/brainstorming/visual-companion.md | 7 +++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 9eb21cab1..2b3c2ccef 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -102,17 +102,25 @@ Save `screen_dir` and `screen_file` from the response. Tell user to open the URL ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR ``` -2. **Write HTML** to `screen_file` using the Write tool (browser auto-refreshes) +2. **Write HTML** to `screen_file`: + - First `Read` the screen_file (even if empty) so Write tool works + - Then use Write tool - **never use cat/heredoc** (dumps noise into terminal) + - If Write fails, read first then retry -3. **Wait for feedback** - call `TaskOutput(task_id, block=true, timeout=600000)` +3. **Tell user what to expect:** + - Remind them of the URL (every step, not just first) + - Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage") + - This lets them know what to look for before switching to browser + +4. **Wait for feedback** - call `TaskOutput(task_id, block=true, timeout=600000)` - If timeout, call TaskOutput again (watcher still running) - After 3 timeouts (30 min), say "Let me know when you want to continue" -4. **Process feedback** - returns JSON like `{"choice": "a", "feedback": "make header smaller"}` +5. **Process feedback** - returns JSON like `{"choice": "a", "feedback": "make header smaller"}` -5. **Iterate or advance** - if feedback changes current screen, update and re-show. Only move to next question when current step is validated. +6. **Iterate or advance** - if feedback changes current screen, update and re-show. Only move to next question when current step is validated. -6. Repeat until done. +7. Repeat until done. ### Cleaning Up diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 37d4f07a0..013ee25ed 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -42,6 +42,13 @@ ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR - **Iterate before advancing** - if feedback changes current screen, update and re-show - **2-4 options max** per screen +## Terminal UX + +- **Never use cat/heredoc for HTML** - dumps noise into terminal. Use Write tool instead. +- **Read screen_file first** before Write (even if empty) to avoid tool errors +- **Remind user of URL** on every step, not just the first +- **Give text summary** of what's on screen before they look (e.g., "Showing 3 API structure options") + ## CSS Classes ### Options (A/B/C choices) From b16369cae235fde8fcd15e06412636a5821ec6e3 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 17 Jan 2026 19:32:02 -0800 Subject: [PATCH 15/19] Use semantic filenames for visual companion screens Server now watches directory for new .html files instead of a single screen file. Claude writes to semantically named files like platform.html, style.html, layout.html - each screen is a new file. Benefits: - No need to read before write (files are always new) - Semantic filenames describe what's on screen - History preserved in directory for debugging - Server serves newest file by mtime automatically Updated: index.js, start-server.sh, and all documentation. --- lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md | 26 ++++---- lib/brainstorm-server/index.js | 62 ++++++++++++++------ lib/brainstorm-server/start-server.sh | 3 +- skills/brainstorming/SKILL.md | 15 ++--- skills/brainstorming/visual-companion.md | 17 ++++-- 5 files changed, 80 insertions(+), 43 deletions(-) diff --git a/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md b/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md index b69d8fa13..9755a8017 100644 --- a/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md +++ b/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md @@ -15,27 +15,33 @@ Use the visual companion when you need to show: ## Lifecycle ```bash -# Start server (returns JSON with URL and session paths) +# Start server (returns JSON with URL and session directory) ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh # Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341", -# "screen_dir":"/tmp/brainstorm-12345-1234567890", -# "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"} +# "screen_dir":"/tmp/brainstorm-12345-1234567890"} -# Save screen_dir and screen_file from response! +# Save screen_dir from response! # Tell user to open the URL in their browser -# Write screens to screen_file (auto-refreshes) - -# Wait for user feedback: -# 1. Start watcher in background -${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log -# 2. Immediately call TaskOutput(task_id, block=true) to wait for completion +# For each screen: +# 1. Start watcher in background FIRST (avoids race condition) +${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR +# 2. Write HTML to a NEW file in screen_dir (e.g., platform.html, style.html) +# Server automatically serves the newest file by modification time +# 3. Call TaskOutput(task_id, block=true, timeout=600000) to wait for feedback # When done, stop server (pass screen_dir) ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR ``` +## File Naming + +- **Use semantic names**: `platform.html`, `visual-style.html`, `layout.html`, `controls.html` +- **Never reuse filenames** - each screen must be a new file +- **For iterations**: append version suffix like `layout-v2.html`, `layout-v3.html` +- Server automatically serves the newest `.html` file by modification time + ## Writing Screens Copy the frame template structure but replace `#claude-content` with your content: diff --git a/lib/brainstorm-server/index.js b/lib/brainstorm-server/index.js index 21390be73..b625b26b6 100644 --- a/lib/brainstorm-server/index.js +++ b/lib/brainstorm-server/index.js @@ -7,17 +7,29 @@ const path = require('path'); // Use provided port or pick a random high port (49152-65535) const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383)); -const SCREEN_FILE = process.env.BRAINSTORM_SCREEN || '/tmp/brainstorm/screen.html'; -const SCREEN_DIR = path.dirname(SCREEN_FILE); +const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; // Ensure screen directory exists if (!fs.existsSync(SCREEN_DIR)) { fs.mkdirSync(SCREEN_DIR, { recursive: true }); } -// Create default screen if none exists -if (!fs.existsSync(SCREEN_FILE)) { - fs.writeFileSync(SCREEN_FILE, ` +// Find the newest .html file in the directory by mtime +function getNewestScreen() { + const files = fs.readdirSync(SCREEN_DIR) + .filter(f => f.endsWith('.html')) + .map(f => ({ + name: f, + path: path.join(SCREEN_DIR, f), + mtime: fs.statSync(path.join(SCREEN_DIR, f)).mtime.getTime() + })) + .sort((a, b) => b.mtime - a.mtime); + + return files.length > 0 ? files[0].path : null; +} + +// Default waiting page (served when no screens exist yet) +const WAITING_PAGE = ` Brainstorm Companion @@ -31,8 +43,7 @@ if (!fs.existsSync(SCREEN_FILE)) {

Brainstorm Companion

Waiting for Claude to push a screen...

-`); -} +`; const app = express(); const server = http.createServer(app); @@ -52,9 +63,10 @@ wss.on('connection', (ws) => { }); }); -// Serve current screen with helper.js injected +// Serve newest screen with helper.js injected app.get('/', (req, res) => { - let html = fs.readFileSync(SCREEN_FILE, 'utf-8'); + const screenFile = getNewestScreen(); + let html = screenFile ? fs.readFileSync(screenFile, 'utf-8') : WAITING_PAGE; // Inject helper script before const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8'); @@ -69,23 +81,35 @@ app.get('/', (req, res) => { res.type('html').send(html); }); -// Watch for screen file changes -chokidar.watch(SCREEN_FILE).on('change', () => { - console.log(JSON.stringify({ type: 'screen-updated', file: SCREEN_FILE })); - // Notify all browsers to reload - clients.forEach(ws => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'reload' })); +// Watch for new or changed .html files in the directory +chokidar.watch(SCREEN_DIR, { ignoreInitial: true }) + .on('add', (filePath) => { + if (filePath.endsWith('.html')) { + console.log(JSON.stringify({ type: 'screen-added', file: filePath })); + // Notify all browsers to reload + clients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'reload' })); + } + }); + } + }) + .on('change', (filePath) => { + if (filePath.endsWith('.html')) { + console.log(JSON.stringify({ type: 'screen-updated', file: filePath })); + clients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'reload' })); + } + }); } }); -}); server.listen(PORT, '127.0.0.1', () => { console.log(JSON.stringify({ type: 'server-started', port: PORT, url: `http://localhost:${PORT}`, - screen_dir: SCREEN_DIR, - screen_file: SCREEN_FILE + screen_dir: SCREEN_DIR })); }); diff --git a/lib/brainstorm-server/start-server.sh b/lib/brainstorm-server/start-server.sh index 1b9992c91..60f2249db 100755 --- a/lib/brainstorm-server/start-server.sh +++ b/lib/brainstorm-server/start-server.sh @@ -11,7 +11,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Generate unique session directory SESSION_ID="$$-$(date +%s)" SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}" -SCREEN_FILE="${SCREEN_DIR}/screen.html" PID_FILE="${SCREEN_DIR}/.server.pid" LOG_FILE="${SCREEN_DIR}/.server.log" @@ -27,7 +26,7 @@ fi # Start server, capturing output to log file cd "$SCRIPT_DIR" -BRAINSTORM_SCREEN="$SCREEN_FILE" node index.js > "$LOG_FILE" 2>&1 & +BRAINSTORM_DIR="$SCREEN_DIR" node index.js > "$LOG_FILE" 2>&1 & SERVER_PID=$! echo "$SERVER_PID" > "$PID_FILE" diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 2b3c2ccef..56b3e3afe 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -90,10 +90,10 @@ Only proceed if they agree. Otherwise, describe options in text. ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh # Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341", -# "screen_dir":"/tmp/brainstorm-12345","screen_file":"/tmp/brainstorm-12345/screen.html"} +# "screen_dir":"/tmp/brainstorm-12345"} ``` -Save `screen_dir` and `screen_file` from the response. Tell user to open the URL. +Save `screen_dir` from the response. Tell user to open the URL. ### The Loop @@ -102,10 +102,11 @@ Save `screen_dir` and `screen_file` from the response. Tell user to open the URL ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR ``` -2. **Write HTML** to `screen_file`: - - First `Read` the screen_file (even if empty) so Write tool works - - Then use Write tool - **never use cat/heredoc** (dumps noise into terminal) - - If Write fails, read first then retry +2. **Write HTML** to a new file in `screen_dir`: + - Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html` + - **Never reuse filenames** - each screen gets a fresh file + - Use Write tool - **never use cat/heredoc** (dumps noise into terminal) + - Server automatically serves the newest file 3. **Tell user what to expect:** - Remind them of the URL (every step, not just first) @@ -118,7 +119,7 @@ Save `screen_dir` and `screen_file` from the response. Tell user to open the URL 5. **Process feedback** - returns JSON like `{"choice": "a", "feedback": "make header smaller"}` -6. **Iterate or advance** - if feedback changes current screen, update and re-show. Only move to next question when current step is validated. +6. **Iterate or advance** - if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to next question when current step is validated. 7. Repeat until done. diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 013ee25ed..71403463e 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -17,18 +17,19 @@ Quick reference for the browser-based visual brainstorming companion. ```bash # 1. Start server ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh -# Returns: {"screen_dir":"/tmp/brainstorm-xxx","screen_file":"...","url":"http://localhost:PORT"} +# Returns: {"screen_dir":"/tmp/brainstorm-xxx","url":"http://localhost:PORT"} # 2. Start watcher FIRST (background bash) - avoids race condition ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR -# 3. Write HTML to screen_file using Write tool (browser auto-refreshes) +# 3. Write HTML to a NEW file in screen_dir (e.g., platform.html, style.html) +# Never reuse filenames - server serves newest file automatically # 4. Call TaskOutput(task_id, block=true, timeout=600000) # If timeout, call again. After 3 timeouts (30 min), prompt user. # Returns: {"choice":"a","feedback":"user notes"} -# 5. Iterate or advance - update screen if feedback changes it, else next question +# 5. Iterate or advance - write new file if feedback changes it (e.g., style-v2.html) # 6. Clean up when done ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR @@ -39,13 +40,19 @@ ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR - **Always ask first** before starting visual companion - **Scale fidelity to the question** - wireframes for layout, polish for polish questions - **Explain the question** on each page - what decision are you seeking? -- **Iterate before advancing** - if feedback changes current screen, update and re-show +- **Iterate before advancing** - if feedback changes current screen, write new version - **2-4 options max** per screen +## File Naming + +- **Use semantic names**: `platform.html`, `visual-style.html`, `layout.html`, `controls.html` +- **Never reuse filenames** - each screen is a new file +- **For iterations**: append version suffix like `layout-v2.html`, `layout-v3.html` +- Server automatically serves the newest file by modification time + ## Terminal UX - **Never use cat/heredoc for HTML** - dumps noise into terminal. Use Write tool instead. -- **Read screen_file first** before Write (even if empty) to avoid tool errors - **Remind user of URL** on every step, not just the first - **Give text summary** of what's on screen before they look (e.g., "Showing 3 API structure options") From 2b8814f7d927f470006420b9d2f74260187e2085 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Mon, 19 Jan 2026 21:03:40 -0800 Subject: [PATCH 16/19] Add instruction priority hierarchy to using-superpowers skill Clarifies that user instructions (CLAUDE.md, direct requests) always take precedence over Superpowers skills, which in turn override default system prompt behavior. Ensures users remain in control. Also updates RELEASE-NOTES.md with unreleased changes including the visual companion feature. --- RELEASE-NOTES.md | 30 ++++++++++++++++++++++++++++++ skills/using-superpowers/SKILL.md | 10 ++++++++++ 2 files changed, 40 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 5ab95451d..52686110b 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,35 @@ # Superpowers Release Notes +## Unreleased + +### New Features + +**Visual companion for brainstorming skill** + +Added optional browser-based visual companion for brainstorming sessions. When users have a browser available, brainstorming can display interactive screens showing current phase, questions, and design decisions in a more readable format than terminal output. + +Components: +- `lib/brainstorm-server/` - WebSocket server for real-time updates +- `skills/brainstorming/visual-companion.md` - Integration guide +- Helper scripts for session management with proper isolation +- Browser helper library for event capture + +The visual companion is opt-in and falls back gracefully to terminal-only operation. + +### Improvements + +**Instruction priority clarified in using-superpowers** + +Added explicit instruction priority hierarchy to prevent conflicts with user preferences: + +1. User's explicit instructions (CLAUDE.md, direct requests) — highest priority +2. Superpowers skills — override default system behavior where they conflict +3. Default system prompt — lowest priority + +This ensures users remain in control. If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," CLAUDE.md wins. + +--- + ## v4.0.3 (2025-12-26) ### Improvements diff --git a/skills/using-superpowers/SKILL.md b/skills/using-superpowers/SKILL.md index 7867fcfc0..1b1c53fc4 100644 --- a/skills/using-superpowers/SKILL.md +++ b/skills/using-superpowers/SKILL.md @@ -11,6 +11,16 @@ IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT. This is not negotiable. This is not optional. You cannot rationalize your way out of this. +## Instruction Priority + +Superpowers skills override default system prompt behavior, but **user instructions always take precedence**: + +1. **User's explicit instructions** (CLAUDE.md, direct requests) — highest priority +2. **Superpowers skills** — override default system behavior where they conflict +3. **Default system prompt** — lowest priority + +If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," follow CLAUDE.md. The user is in control. + ## How to Access Skills **In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files. From 7a6b4c14d5149eb9c603a8f85eb7dff65e460ba7 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 22 Jan 2026 13:29:30 -0800 Subject: [PATCH 17/19] feat(opencode): use native skills and fix agent reset bug (#226) (#330) * fix use_skill agent context (#290) * fix: respect OPENCODE_CONFIG_DIR for personal skills lookup (#297) * fix: respect OPENCODE_CONFIG_DIR for personal skills lookup The plugin was hardcoded to look for personal skills in ~/.config/opencode/skills, ignoring users who set OPENCODE_CONFIG_DIR to a custom path (e.g., for dotfiles management). Now uses OPENCODE_CONFIG_DIR if set, falling back to the default path. * fix: update help text to use dynamic paths Use configDir and personalSkillsDir variables in help text so paths are accurate when OPENCODE_CONFIG_DIR is set. * fix: normalize OPENCODE_CONFIG_DIR before use Handle edge cases where the env var might be: - Empty or whitespace-only - Using ~ for home directory (common in .env files) - A relative path Now trims, expands ~, and resolves to absolute path. * feat(opencode): use native skills and fix agent reset bug (#226) - Replace custom use_skill/find_skills tools with OpenCode's native skill tool - Use experimental.chat.system.transform hook instead of session.prompt (fixes #226 agent reset on first message) - Symlink skills directory into ~/.config/opencode/skills/superpowers/ - Update installation docs with comprehensive Windows support: - Command Prompt, PowerShell, and Git Bash instructions - Proper symlink vs junction handling - Reinstall safety with cleanup steps - Verification commands for each shell * Add OpenCode native skills changes to release notes Documents: - Breaking change: switch to native skill tool - Fix for agent reset bug (#226) - Fix for Windows installation (#232) --------- Co-authored-by: Vinicius da Motta Co-authored-by: oribi --- .opencode/plugin/superpowers.js | 230 +++++++---------------------- RELEASE-NOTES.md | 20 +++ docs/README.opencode.md | 250 ++++++++++++++++++++++---------- 3 files changed, 246 insertions(+), 254 deletions(-) diff --git a/.opencode/plugin/superpowers.js b/.opencode/plugin/superpowers.js index c9a6e29ea..8ac993462 100644 --- a/.opencode/plugin/superpowers.js +++ b/.opencode/plugin/superpowers.js @@ -1,55 +1,81 @@ /** * Superpowers plugin for OpenCode.ai * - * Provides custom tools for loading and discovering skills, - * with prompt generation for agent configuration. + * Injects superpowers bootstrap context via system prompt transform. + * Skills are discovered via OpenCode's native skill tool from symlinked directory. */ import path from 'path'; import fs from 'fs'; import os from 'os'; import { fileURLToPath } from 'url'; -import { tool } from '@opencode-ai/plugin/tool'; -import * as skillsCore from '../../lib/skills-core.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// Simple frontmatter extraction (avoid dependency on skills-core for bootstrap) +const extractAndStripFrontmatter = (content) => { + const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!match) return { frontmatter: {}, content }; + + const frontmatterStr = match[1]; + const body = match[2]; + const frontmatter = {}; + + for (const line of frontmatterStr.split('\n')) { + const colonIdx = line.indexOf(':'); + if (colonIdx > 0) { + const key = line.slice(0, colonIdx).trim(); + const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, ''); + frontmatter[key] = value; + } + } + + return { frontmatter, content: body }; +}; + +// Normalize a path: trim whitespace, expand ~, resolve to absolute +const normalizePath = (p, homeDir) => { + if (!p || typeof p !== 'string') return null; + let normalized = p.trim(); + if (!normalized) return null; + if (normalized.startsWith('~/')) { + normalized = path.join(homeDir, normalized.slice(2)); + } else if (normalized === '~') { + normalized = homeDir; + } + return path.resolve(normalized); +}; + export const SuperpowersPlugin = async ({ client, directory }) => { const homeDir = os.homedir(); - const projectSkillsDir = path.join(directory, '.opencode/skills'); - // Derive superpowers skills dir from plugin location (works for both symlinked and local installs) const superpowersSkillsDir = path.resolve(__dirname, '../../skills'); - const personalSkillsDir = path.join(homeDir, '.config/opencode/skills'); + const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir); + const configDir = envConfigDir || path.join(homeDir, '.config/opencode'); // Helper to generate bootstrap content - const getBootstrapContent = (compact = false) => { - const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir); - if (!usingSuperpowersPath) return null; + const getBootstrapContent = () => { + // Try to load using-superpowers skill + const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md'); + if (!fs.existsSync(skillPath)) return null; - const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8'); - const content = skillsCore.stripFrontmatter(fullContent); + const fullContent = fs.readFileSync(skillPath, 'utf8'); + const { content } = extractAndStripFrontmatter(fullContent); - const toolMapping = compact - ? `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill - -**Skills naming (priority order):** project: > personal > superpowers:` - : `**Tool Mapping for OpenCode:** + const toolMapping = `**Tool Mapping for OpenCode:** When skills reference tools you don't have, substitute OpenCode equivalents: - \`TodoWrite\` → \`update_plan\` - \`Task\` tool with subagents → Use OpenCode's subagent system (@mention) -- \`Skill\` tool → \`use_skill\` custom tool +- \`Skill\` tool → OpenCode's native \`skill\` tool - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools -**Skills naming (priority order):** -- Project skills: \`project:skill-name\` (in .opencode/skills/) -- Personal skills: \`skill-name\` (in ~/.config/opencode/skills/) -- Superpowers skills: \`superpowers:skill-name\` -- Project skills override personal, which override superpowers when names match`; +**Skills location:** +Superpowers skills are in \`${configDir}/skills/superpowers/\` +Use OpenCode's native \`skill\` tool to list and load skills.`; return ` You have superpowers. -**IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the use_skill tool to load "using-superpowers" - that would be redundant. Use use_skill only for OTHER skills.** +**IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the skill tool to load "using-superpowers" again - that would be redundant.** ${content} @@ -57,158 +83,12 @@ ${toolMapping} `; }; - // Helper to inject bootstrap via session.prompt - const injectBootstrap = async (sessionID, compact = false) => { - const bootstrapContent = getBootstrapContent(compact); - if (!bootstrapContent) return false; - - try { - await client.session.prompt({ - path: { id: sessionID }, - body: { - noReply: true, - parts: [{ type: "text", text: bootstrapContent, synthetic: true }] - } - }); - return true; - } catch (err) { - return false; - } - }; - return { - tool: { - use_skill: tool({ - description: 'Load and read a specific skill to guide your work. Skills contain proven workflows, mandatory processes, and expert techniques.', - args: { - skill_name: tool.schema.string().describe('Name of the skill to load (e.g., "superpowers:brainstorming", "my-custom-skill", or "project:my-skill")') - }, - execute: async (args, context) => { - const { skill_name } = args; - - // Resolve with priority: project > personal > superpowers - // Check for project: prefix first - const forceProject = skill_name.startsWith('project:'); - const actualSkillName = forceProject ? skill_name.replace(/^project:/, '') : skill_name; - - let resolved = null; - - // Try project skills first (if project: prefix or no prefix) - if (forceProject || !skill_name.startsWith('superpowers:')) { - const projectPath = path.join(projectSkillsDir, actualSkillName); - const projectSkillFile = path.join(projectPath, 'SKILL.md'); - if (fs.existsSync(projectSkillFile)) { - resolved = { - skillFile: projectSkillFile, - sourceType: 'project', - skillPath: actualSkillName - }; - } - } - - // Fall back to personal/superpowers resolution - if (!resolved && !forceProject) { - resolved = skillsCore.resolveSkillPath(skill_name, superpowersSkillsDir, personalSkillsDir); - } - - if (!resolved) { - return `Error: Skill "${skill_name}" not found.\n\nRun find_skills to see available skills.`; - } - - const fullContent = fs.readFileSync(resolved.skillFile, 'utf8'); - const { name, description } = skillsCore.extractFrontmatter(resolved.skillFile); - const content = skillsCore.stripFrontmatter(fullContent); - const skillDirectory = path.dirname(resolved.skillFile); - - const skillHeader = `# ${name || skill_name} -# ${description || ''} -# Supporting tools and docs are in ${skillDirectory} -# ============================================`; - - // Insert as user message with noReply for persistence across compaction - try { - await client.session.prompt({ - path: { id: context.sessionID }, - body: { - noReply: true, - parts: [ - { type: "text", text: `Loading skill: ${name || skill_name}`, synthetic: true }, - { type: "text", text: `${skillHeader}\n\n${content}`, synthetic: true } - ] - } - }); - } catch (err) { - // Fallback: return content directly if message insertion fails - return `${skillHeader}\n\n${content}`; - } - - return `Launching skill: ${name || skill_name}`; - } - }), - find_skills: tool({ - description: 'List all available skills in the project, personal, and superpowers skill libraries.', - args: {}, - execute: async (args, context) => { - const projectSkills = skillsCore.findSkillsInDir(projectSkillsDir, 'project', 3); - const personalSkills = skillsCore.findSkillsInDir(personalSkillsDir, 'personal', 3); - const superpowersSkills = skillsCore.findSkillsInDir(superpowersSkillsDir, 'superpowers', 3); - - // Priority: project > personal > superpowers - const allSkills = [...projectSkills, ...personalSkills, ...superpowersSkills]; - - if (allSkills.length === 0) { - return 'No skills found. Install superpowers skills to ~/.config/opencode/superpowers/skills/ or add project skills to .opencode/skills/'; - } - - let output = 'Available skills:\n\n'; - - for (const skill of allSkills) { - let namespace; - switch (skill.sourceType) { - case 'project': - namespace = 'project:'; - break; - case 'personal': - namespace = ''; - break; - default: - namespace = 'superpowers:'; - } - const skillName = skill.name || path.basename(skill.path); - - output += `${namespace}${skillName}\n`; - if (skill.description) { - output += ` ${skill.description}\n`; - } - output += ` Directory: ${skill.path}\n\n`; - } - - return output; - } - }) - }, - event: async ({ event }) => { - // Extract sessionID from various event structures - const getSessionID = () => { - return event.properties?.info?.id || - event.properties?.sessionID || - event.session?.id; - }; - - // Inject bootstrap at session creation (before first user message) - if (event.type === 'session.created') { - const sessionID = getSessionID(); - if (sessionID) { - await injectBootstrap(sessionID, false); - } - } - - // Re-inject bootstrap after context compaction (compact version to save tokens) - if (event.type === 'session.compacted') { - const sessionID = getSessionID(); - if (sessionID) { - await injectBootstrap(sessionID, true); - } + // Use system prompt transform to inject bootstrap (fixes #226 agent reset bug) + 'experimental.chat.system.transform': async (_input, output) => { + const bootstrap = getBootstrapContent(); + if (bootstrap) { + (output.system ||= []).push(bootstrap); } } }; diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 52686110b..4542f6549 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -2,6 +2,26 @@ ## Unreleased +### Breaking Changes + +**OpenCode: Switched to native skills system** + +Superpowers for OpenCode now uses OpenCode's native `skill` tool instead of custom `use_skill`/`find_skills` tools. This is a cleaner integration that works with OpenCode's built-in skill discovery. + +**Migration required:** Skills must be symlinked to `~/.config/opencode/skills/superpowers/` (see updated installation docs). + +### Fixes + +**OpenCode: Fixed agent reset on session start (#226)** + +The previous bootstrap injection method using `session.prompt({ noReply: true })` caused OpenCode to reset the selected agent to "build" on first message. Now uses `experimental.chat.system.transform` hook which modifies the system prompt directly without side effects. + +**OpenCode: Fixed Windows installation (#232)** + +- Removed dependency on `skills-core.js` (eliminates broken relative imports when file is copied instead of symlinked) +- Added comprehensive Windows installation docs for cmd.exe, PowerShell, and Git Bash +- Documented proper symlink vs junction usage for each platform + ### New Features **Visual companion for brainstorming skill** diff --git a/docs/README.opencode.md b/docs/README.opencode.md index 122fe55ea..4370625a8 100644 --- a/docs/README.opencode.md +++ b/docs/README.opencode.md @@ -7,7 +7,7 @@ Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai). Tell OpenCode: ``` -Clone https://github.com/obra/superpowers to ~/.config/opencode/superpowers, then create directory ~/.config/opencode/plugin, then symlink ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js to ~/.config/opencode/plugin/superpowers.js, then restart opencode. +Clone https://github.com/obra/superpowers to ~/.config/opencode/superpowers, then create directory ~/.config/opencode/plugin, then symlink ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js to ~/.config/opencode/plugin/superpowers.js, then symlink ~/.config/opencode/superpowers/skills to ~/.config/opencode/skills/superpowers, then restart opencode. ``` ## Manual Installation @@ -15,59 +15,171 @@ Clone https://github.com/obra/superpowers to ~/.config/opencode/superpowers, the ### Prerequisites - [OpenCode.ai](https://opencode.ai) installed -- Node.js installed - Git installed -### Installation Steps +### macOS / Linux -#### 1. Install Superpowers +```bash +# 1. Install Superpowers (or update existing) +if [ -d ~/.config/opencode/superpowers ]; then + cd ~/.config/opencode/superpowers && git pull +else + git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers +fi + +# 2. Create directories +mkdir -p ~/.config/opencode/plugin ~/.config/opencode/skills + +# 3. Create symlinks (safe for reinstalls - ln -sf overwrites) +ln -sf ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js ~/.config/opencode/plugin/superpowers.js +ln -sf ~/.config/opencode/superpowers/skills ~/.config/opencode/skills/superpowers + +# 4. Restart OpenCode +``` + +#### Verify Installation ```bash -mkdir -p ~/.config/opencode/superpowers -git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers +ls -l ~/.config/opencode/plugin/superpowers.js +ls -l ~/.config/opencode/skills/superpowers ``` -#### 2. Register the Plugin +Both should show symlinks pointing to the superpowers directory. -OpenCode discovers plugins from `~/.config/opencode/plugin/`. Create a symlink: +### Windows -```bash -mkdir -p ~/.config/opencode/plugin -ln -sf ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js ~/.config/opencode/plugin/superpowers.js +**Prerequisites:** +- Git installed +- Either **Developer Mode** enabled OR **Administrator privileges** + - Windows 10: Settings → Update & Security → For developers + - Windows 11: Settings → System → For developers + +Pick your shell below: [Command Prompt](#command-prompt) | [PowerShell](#powershell) | [Git Bash](#git-bash) + +#### Command Prompt + +Run as Administrator, or with Developer Mode enabled: + +```cmd +:: 1. Install Superpowers +git clone https://github.com/obra/superpowers.git "%USERPROFILE%\.config\opencode\superpowers" + +:: 2. Create directories +mkdir "%USERPROFILE%\.config\opencode\plugin" 2>nul +mkdir "%USERPROFILE%\.config\opencode\skills" 2>nul + +:: 3. Remove existing links (safe for reinstalls) +del "%USERPROFILE%\.config\opencode\plugin\superpowers.js" 2>nul +rmdir "%USERPROFILE%\.config\opencode\skills\superpowers" 2>nul + +:: 4. Create plugin symlink (requires Developer Mode or Admin) +mklink "%USERPROFILE%\.config\opencode\plugin\superpowers.js" "%USERPROFILE%\.config\opencode\superpowers\.opencode\plugin\superpowers.js" + +:: 5. Create skills junction (works without special privileges) +mklink /J "%USERPROFILE%\.config\opencode\skills\superpowers" "%USERPROFILE%\.config\opencode\superpowers\skills" + +:: 6. Restart OpenCode ``` -Alternatively, for project-local installation: +#### PowerShell + +Run as Administrator, or with Developer Mode enabled: + +```powershell +# 1. Install Superpowers +git clone https://github.com/obra/superpowers.git "$env:USERPROFILE\.config\opencode\superpowers" + +# 2. Create directories +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode\plugin" +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode\skills" + +# 3. Remove existing links (safe for reinstalls) +Remove-Item "$env:USERPROFILE\.config\opencode\plugin\superpowers.js" -Force -ErrorAction SilentlyContinue +Remove-Item "$env:USERPROFILE\.config\opencode\skills\superpowers" -Force -ErrorAction SilentlyContinue + +# 4. Create plugin symlink (requires Developer Mode or Admin) +New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.config\opencode\plugin\superpowers.js" -Target "$env:USERPROFILE\.config\opencode\superpowers\.opencode\plugin\superpowers.js" + +# 5. Create skills junction (works without special privileges) +New-Item -ItemType Junction -Path "$env:USERPROFILE\.config\opencode\skills\superpowers" -Target "$env:USERPROFILE\.config\opencode\superpowers\skills" + +# 6. Restart OpenCode +``` + +#### Git Bash + +Note: Git Bash's native `ln` command copies files instead of creating symlinks. Use `cmd //c mklink` instead (the `//c` is Git Bash syntax for `/c`). ```bash -# In your OpenCode project -mkdir -p .opencode/plugin -ln -sf ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js .opencode/plugin/superpowers.js +# 1. Install Superpowers +git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers + +# 2. Create directories +mkdir -p ~/.config/opencode/plugin ~/.config/opencode/skills + +# 3. Remove existing links (safe for reinstalls) +rm -f ~/.config/opencode/plugin/superpowers.js 2>/dev/null +rm -rf ~/.config/opencode/skills/superpowers 2>/dev/null + +# 4. Create plugin symlink (requires Developer Mode or Admin) +cmd //c "mklink \"$(cygpath -w ~/.config/opencode/plugin/superpowers.js)\" \"$(cygpath -w ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js)\"" + +# 5. Create skills junction (works without special privileges) +cmd //c "mklink /J \"$(cygpath -w ~/.config/opencode/skills/superpowers)\" \"$(cygpath -w ~/.config/opencode/superpowers/skills)\"" + +# 6. Restart OpenCode +``` + +#### WSL Users + +If running OpenCode inside WSL, use the [macOS / Linux](#macos--linux) instructions instead. + +#### Verify Installation + +**Command Prompt:** +```cmd +dir /AL "%USERPROFILE%\.config\opencode\plugin" +dir /AL "%USERPROFILE%\.config\opencode\skills" +``` + +**PowerShell:** +```powershell +Get-ChildItem "$env:USERPROFILE\.config\opencode\plugin" | Where-Object { $_.LinkType } +Get-ChildItem "$env:USERPROFILE\.config\opencode\skills" | Where-Object { $_.LinkType } ``` -#### 3. Restart OpenCode +Look for `` or `` in the output. + +#### Troubleshooting Windows + +**"You do not have sufficient privilege" error:** +- Enable Developer Mode in Windows Settings, OR +- Right-click your terminal → "Run as Administrator" + +**"Cannot create a file when that file already exists":** +- Run the removal commands (step 3) first, then retry -Restart OpenCode to load the plugin. Superpowers will automatically activate. +**Symlinks not working after git clone:** +- Run `git config --global core.symlinks true` and re-clone ## Usage ### Finding Skills -Use the `find_skills` tool to list all available skills: +Use OpenCode's native `skill` tool to list all available skills: ``` -use find_skills tool +use skill tool to list skills ``` ### Loading a Skill -Use the `use_skill` tool to load a specific skill: +Use OpenCode's native `skill` tool to load a specific skill: ``` -use use_skill tool with skill_name: "superpowers:brainstorming" +use skill tool to load superpowers/brainstorming ``` -Skills are automatically inserted into the conversation and persist across context compaction. - ### Personal Skills Create your own skills in `~/.config/opencode/skills/`: @@ -111,40 +223,31 @@ description: Use when [condition] - [what it does] [Your skill content here] ``` -## Skill Priority +## Skill Locations -Skills are resolved with this priority order: +OpenCode discovers skills from these locations: 1. **Project skills** (`.opencode/skills/`) - Highest priority 2. **Personal skills** (`~/.config/opencode/skills/`) -3. **Superpowers skills** (`~/.config/opencode/superpowers/skills/`) - -You can force resolution to a specific level: -- `project:skill-name` - Force project skill -- `skill-name` - Search project → personal → superpowers -- `superpowers:skill-name` - Force superpowers skill +3. **Superpowers skills** (`~/.config/opencode/skills/superpowers/`) - via symlink ## Features ### Automatic Context Injection -The plugin automatically injects superpowers context via the chat.message hook on every session. No manual configuration needed. - -### Message Insertion Pattern +The plugin automatically injects superpowers context via the `experimental.chat.system.transform` hook. This adds the "using-superpowers" skill content to the system prompt on every request. -When you load a skill with `use_skill`, it's inserted as a user message with `noReply: true`. This ensures skills persist throughout long conversations, even when OpenCode compacts context. +### Native Skills Integration -### Compaction Resilience - -The plugin listens for `session.compacted` events and automatically re-injects the core superpowers bootstrap to maintain functionality after context compaction. +Superpowers uses OpenCode's native `skill` tool for skill discovery and loading. Skills are symlinked into `~/.config/opencode/skills/superpowers/` so they appear alongside your personal and project skills. ### Tool Mapping -Skills written for Claude Code are automatically adapted for OpenCode. The plugin provides mapping instructions: +Skills written for Claude Code are automatically adapted for OpenCode. The bootstrap provides mapping instructions: - `TodoWrite` → `update_plan` - `Task` with subagents → OpenCode's `@mention` system -- `Skill` tool → `use_skill` custom tool +- `Skill` tool → OpenCode's native `skill` tool - File operations → Native OpenCode tools ## Architecture @@ -154,23 +257,14 @@ Skills written for Claude Code are automatically adapted for OpenCode. The plugi **Location:** `~/.config/opencode/superpowers/.opencode/plugin/superpowers.js` **Components:** -- Two custom tools: `use_skill`, `find_skills` -- chat.message hook for initial context injection -- event handler for session.compacted re-injection -- Uses shared `lib/skills-core.js` module (also used by Codex) - -### Shared Core Module +- `experimental.chat.system.transform` hook for bootstrap injection +- Reads and injects the "using-superpowers" skill content -**Location:** `~/.config/opencode/superpowers/lib/skills-core.js` +### Skills -**Functions:** -- `extractFrontmatter()` - Parse skill metadata -- `stripFrontmatter()` - Remove metadata from content -- `findSkillsInDir()` - Recursive skill discovery -- `resolveSkillPath()` - Skill resolution with shadowing -- `checkForUpdates()` - Git update detection +**Location:** `~/.config/opencode/skills/superpowers/` (symlink to `~/.config/opencode/superpowers/skills/`) -This module is shared between OpenCode and Codex implementations for code reuse. +Skills are discovered by OpenCode's native skill system. Each skill has a `SKILL.md` file with YAML frontmatter. ## Updating @@ -185,28 +279,28 @@ Restart OpenCode to load the updates. ### Plugin not loading -1. Check plugin file exists: `ls ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js` -2. Check symlink: `ls -l ~/.config/opencode/plugin/superpowers.js` +1. Check plugin exists: `ls ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js` +2. Check symlink/junction: `ls -l ~/.config/opencode/plugin/` (macOS/Linux) or `dir /AL %USERPROFILE%\.config\opencode\plugin` (Windows) 3. Check OpenCode logs: `opencode run "test" --print-logs --log-level DEBUG` -4. Look for: `service=plugin path=file:///.../superpowers.js loading plugin` +4. Look for plugin loading message in logs ### Skills not found -1. Verify skills directory: `ls ~/.config/opencode/superpowers/skills` -2. Use `find_skills` tool to see what's discovered -3. Check skill structure: each skill needs a `SKILL.md` file +1. Verify skills symlink: `ls -l ~/.config/opencode/skills/superpowers` (should point to superpowers/skills/) +2. Use OpenCode's `skill` tool to list available skills +3. Check skill structure: each skill needs a `SKILL.md` file with valid frontmatter -### Tools not working +### Windows: Module not found error -1. Verify plugin loaded: Check OpenCode logs for plugin loading message -2. Check Node.js version: The plugin requires Node.js for ES modules -3. Test plugin manually: `node --input-type=module -e "import('file://~/.config/opencode/plugin/superpowers.js').then(m => console.log(Object.keys(m)))"` +If you see `Cannot find module` errors on Windows: +- **Cause:** Git Bash `ln -sf` copies files instead of creating symlinks +- **Fix:** Use `mklink /J` directory junctions instead (see Windows installation steps) -### Context not injecting +### Bootstrap not appearing -1. Check if chat.message hook is working -2. Verify using-superpowers skill exists -3. Check OpenCode version (requires recent version with plugin support) +1. Verify using-superpowers skill exists: `ls ~/.config/opencode/superpowers/skills/using-superpowers/SKILL.md` +2. Check OpenCode version supports `experimental.chat.system.transform` hook +3. Restart OpenCode after plugin changes ## Getting Help @@ -216,19 +310,17 @@ Restart OpenCode to load the updates. ## Testing -The implementation includes an automated test suite at `tests/opencode/`: +Verify your installation: ```bash -# Run all tests -./tests/opencode/run-tests.sh --integration --verbose +# Check plugin loads +opencode run --print-logs "hello" 2>&1 | grep -i superpowers + +# Check skills are discoverable +opencode run "use skill tool to list all skills" 2>&1 | grep -i superpowers -# Run specific test -./tests/opencode/run-tests.sh --test test-tools.sh +# Check bootstrap injection +opencode run "what superpowers do you have?" ``` -Tests verify: -- Plugin loading -- Skills-core library functionality -- Tool execution (use_skill, find_skills) -- Skill priority resolution -- Proper isolation with temp HOME +The agent should mention having superpowers and be able to list skills from `superpowers/`. From e147c303c00a5ca36f61cb8826b5cb72e0884aff Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 22 Jan 2026 13:29:41 -0800 Subject: [PATCH 18/19] fix: Windows hook execution for Claude Code 2.1.x (#331) * fix: convert shell scripts from CRLF to LF line endings Add .gitattributes to enforce LF line endings for shell scripts, preventing bash errors like "/usr/bin/bash: line 1: : command not found" when scripts are checked out on Windows with CRLF. Fixes #317 (SessionStart hook fails due to CRLF line endings) Files converted: - hooks/session-start.sh - lib/brainstorm-server/start-server.sh - lib/brainstorm-server/stop-server.sh - lib/brainstorm-server/wait-for-feedback.sh - skills/systematic-debugging/find-polluter.sh Co-Authored-By: Claude Opus 4.5 * fix: update Windows hook execution for Claude Code 2.1.x Claude Code 2.1.x changed the Windows execution model: it now auto-detects .sh files in hook commands and prepends "bash " automatically. This broke the polyglot wrapper because: Before: "run-hook.cmd" session-start.sh (wrapper executes) After: bash "run-hook.cmd" session-start.sh (bash can't run .cmd) Changes: - hooks.json now calls session-start.sh directly (Claude Code handles bash) - Added deprecation comment to run-hook.cmd explaining the change - Updated RELEASE-NOTES.md Fixes #317, #313, #275, #292 Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .gitattributes | 17 +++++++++++++++++ RELEASE-NOTES.md | 8 ++++++++ hooks/hooks.json | 2 +- hooks/run-hook.cmd | 26 +++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..7387a83b3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Ensure shell scripts always have LF line endings +*.sh text eol=lf + +# Ensure the polyglot wrapper keeps LF (it's parsed by both cmd and bash) +*.cmd text eol=lf + +# Common text files +*.md text eol=lf +*.json text eol=lf +*.js text eol=lf +*.mjs text eol=lf +*.ts text eol=lf + +# Explicitly mark binary files +*.png binary +*.jpg binary +*.gif binary diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4542f6549..ea269f6a9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -36,6 +36,14 @@ Components: The visual companion is opt-in and falls back gracefully to terminal-only operation. +### Bug Fixes + +**Fixed Windows hook execution for Claude Code 2.1.x** + +Claude Code 2.1.x changed how hooks execute on Windows: it now auto-detects `.sh` files in commands and prepends `bash `. This broke the polyglot wrapper pattern because `bash "run-hook.cmd" session-start.sh` tries to execute the .cmd file as a bash script. + +Fix: hooks.json now calls session-start.sh directly. Claude Code 2.1.x handles the bash invocation automatically. Also added .gitattributes to enforce LF line endings for shell scripts (fixes CRLF issues on Windows checkout). + ### Improvements **Instruction priority clarified in using-superpowers** diff --git a/hooks/hooks.json b/hooks/hooks.json index d1745650c..17e0ac87b 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh" } ] } diff --git a/hooks/run-hook.cmd b/hooks/run-hook.cmd index 8d8458fcb..b2a8b3acc 100755 --- a/hooks/run-hook.cmd +++ b/hooks/run-hook.cmd @@ -1,6 +1,30 @@ : << 'CMDBLOCK' @echo off -REM Polyglot wrapper: runs .sh scripts cross-platform +REM ============================================================================ +REM DEPRECATED: This polyglot wrapper is no longer used as of Claude Code 2.1.x +REM ============================================================================ +REM +REM Claude Code 2.1.x changed the Windows execution model for hooks: +REM +REM Before (2.0.x): Hooks ran with shell:true, using the system default shell. +REM This wrapper provided cross-platform compatibility by +REM being both a valid .cmd file (Windows) and bash script. +REM +REM After (2.1.x): Claude Code now auto-detects .sh files in hook commands +REM and prepends "bash " on Windows. This broke the wrapper +REM because the command: +REM "run-hook.cmd" session-start.sh +REM became: +REM bash "run-hook.cmd" session-start.sh +REM ...and bash cannot execute a .cmd file. +REM +REM The fix: hooks.json now calls session-start.sh directly. Claude Code 2.1.x +REM handles the bash invocation automatically on Windows. +REM +REM This file is kept for reference and potential backward compatibility. +REM ============================================================================ +REM +REM Original purpose: Polyglot wrapper to run .sh scripts cross-platform REM Usage: run-hook.cmd [args...] REM The script should be in the same directory as this wrapper From c3d478dc47c1f5bcc4c8eb85ead8e5a537a9b68f Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 22 Jan 2026 14:07:04 -0800 Subject: [PATCH 19/19] fix: add Windows launcher for Codex CLI (#243, #285) Windows cannot execute extensionless scripts with shebangs. Added .cmd wrapper that invokes Node.js directly. Changes: - Add .codex/superpowers-codex.cmd (Windows shim) - Update docs/README.codex.md with Windows installation instructions - Add Windows troubleshooting section Co-Authored-By: Claude Opus 4.5 --- .codex/superpowers-codex.cmd | 14 ++++++++++++ RELEASE-NOTES.md | 6 ++++++ docs/README.codex.md | 42 +++++++++++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 .codex/superpowers-codex.cmd diff --git a/.codex/superpowers-codex.cmd b/.codex/superpowers-codex.cmd new file mode 100644 index 000000000..a214cbe95 --- /dev/null +++ b/.codex/superpowers-codex.cmd @@ -0,0 +1,14 @@ +@echo off +setlocal + +REM Windows shim for the extensionless Node.js launcher (superpowers-codex). +REM +REM Windows cannot execute extensionless scripts with shebangs, so this wrapper +REM invokes Node.js directly. +REM +REM Usage: +REM superpowers-codex.cmd bootstrap +REM superpowers-codex.cmd use-skill superpowers:brainstorming +REM superpowers-codex.cmd find-skills + +node "%~dp0superpowers-codex" %* diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index ea269f6a9..9751bebbe 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -44,6 +44,12 @@ Claude Code 2.1.x changed how hooks execute on Windows: it now auto-detects `.sh Fix: hooks.json now calls session-start.sh directly. Claude Code 2.1.x handles the bash invocation automatically. Also added .gitattributes to enforce LF line endings for shell scripts (fixes CRLF issues on Windows checkout). +**Fixed Windows Codex launcher (#243, #285)** + +Windows cannot execute extensionless scripts with shebangs, so the `superpowers-codex` script would either open an "Open with" dialog or produce no output in PowerShell. + +Fix: Added `.codex/superpowers-codex.cmd` wrapper that invokes Node.js directly. Updated docs with Windows-specific installation and usage instructions. + ### Improvements **Instruction priority clarified in using-superpowers** diff --git a/docs/README.codex.md b/docs/README.codex.md index e43004f42..dbf3de43f 100644 --- a/docs/README.codex.md +++ b/docs/README.codex.md @@ -19,13 +19,27 @@ Fetch and follow instructions from https://raw.githubusercontent.com/obra/superp ### Installation Steps -#### 1. Clone Superpowers +#### macOS / Linux ```bash mkdir -p ~/.codex/superpowers git clone https://github.com/obra/superpowers.git ~/.codex/superpowers ``` +#### Windows + +**Command Prompt:** +```cmd +mkdir "%USERPROFILE%\.codex\superpowers" +git clone https://github.com/obra/superpowers.git "%USERPROFILE%\.codex\superpowers" +``` + +**PowerShell:** +```powershell +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.codex\superpowers" +git clone https://github.com/obra/superpowers.git "$env:USERPROFILE\.codex\superpowers" +``` + #### 2. Install Bootstrap The bootstrap file is included in the repository at `.codex/superpowers-bootstrap.md`. Codex will automatically use it from the cloned location. @@ -34,12 +48,20 @@ The bootstrap file is included in the repository at `.codex/superpowers-bootstra Tell Codex: +**macOS / Linux:** ``` Run ~/.codex/superpowers/.codex/superpowers-codex find-skills to show available skills ``` +**Windows:** +``` +Run ~/.codex/superpowers/.codex/superpowers-codex.cmd find-skills to show available skills +``` + You should see a list of available skills with descriptions. +> **Note:** On Windows, always use the `.cmd` extension when running superpowers-codex commands. + ## Usage ### Finding Skills @@ -126,12 +148,26 @@ git pull 2. Check CLI works: `~/.codex/superpowers/.codex/superpowers-codex find-skills` 3. Verify skills have SKILL.md files -### CLI script not executable +### CLI script not executable (macOS/Linux) ```bash chmod +x ~/.codex/superpowers/.codex/superpowers-codex ``` +### Windows: "Open with" dialog or no output + +On Windows, you must use the `.cmd` wrapper: + +```cmd +~/.codex/superpowers/.codex/superpowers-codex.cmd find-skills +``` + +Or invoke with Node directly: + +```cmd +node "%USERPROFILE%\.codex\superpowers\.codex\superpowers-codex" find-skills +``` + ### Node.js errors The CLI script requires Node.js. Verify: @@ -140,7 +176,7 @@ The CLI script requires Node.js. Verify: node --version ``` -Should show v14 or higher (v18+ recommended for ES module support). +Should show v14 or higher (v18+ recommended). ## Getting Help