brainstorm-companion 2.0.1 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,10 +68,9 @@ brainstorm-companion stop
68
68
  ```
69
69
 
70
70
  **Key behaviors:**
71
- - `start` with no args works immediately (stores in `/tmp/brainstorm-companion/`)
71
+ - `start` with no args works immediately auto-isolates by working directory
72
72
  - `start` reuses an existing session — never opens duplicate browsers
73
73
  - `push` auto-reloads the browser every time — no restart or refresh needed
74
- - Add `--project-dir .` for project-local storage
75
74
 
76
75
  ### Quick Start (MCP / Agent) — 3 calls, zero config
77
76
 
@@ -90,7 +89,7 @@ brainstorm-companion stop
90
89
  5. brainstorm_stop_session({})
91
90
  ```
92
91
 
93
- **No arguments required.** `brainstorm_start_session()` works with zero config. Pass `project_dir` for project-local storage or when multiple agents run simultaneously. Sessions persist until `brainstorm_stop_session()` — use `idle_timeout_minutes` for auto-cleanup.
92
+ **No arguments required.** Sessions auto-isolate by working directory different projects never collide. Sessions persist until `brainstorm_stop_session()` — use `idle_timeout_minutes` for auto-cleanup.
94
93
 
95
94
  ---
96
95
 
@@ -231,9 +230,9 @@ graph TD
231
230
 
232
231
  | Tool | Description |
233
232
  |------|-------------|
234
- | `brainstorm_start_session` | Start server (or reuse existing). Returns URL. Always pass `project_dir`. |
233
+ | `brainstorm_start_session` | Start server (or reuse existing). No args needed. Returns URL. |
235
234
  | `brainstorm_push_screen` | Push HTML content. Browser auto-reloads. Use `slot` + `label` for comparison. |
236
- | `brainstorm_read_events` | Read user interaction events. Option to clear after reading. |
235
+ | `brainstorm_read_events` | Read user interactions. Use `wait_seconds: 120` to get clicks automatically. Never blocks other tools. |
237
236
  | `brainstorm_clear_screen` | Clear a specific slot or all content. |
238
237
  | `brainstorm_stop_session` | Stop server and clean up session files. |
239
238
 
@@ -244,30 +243,28 @@ graph TD
244
243
  ### Single Decision
245
244
 
246
245
  ```
247
- 1. brainstorm_start_session({ project_dir: "..." })
246
+ 1. brainstorm_start_session()
248
247
  2. brainstorm_push_screen({ html: "...options with data-choice..." })
249
- 3. Tell user to make their selection in the browser
250
- 4. brainstorm_read_events({})
251
- 5. → Use the choice to proceed
252
- 6. brainstorm_stop_session({})
248
+ 3. brainstorm_read_events({ wait_seconds: 120 }) // returns when user clicks
249
+ 4. → User's choice arrives automatically
250
+ 5. brainstorm_stop_session()
253
251
  ```
254
252
 
255
253
  ### A/B/C Comparison
256
254
 
257
255
  ```
258
- 1. brainstorm_start_session({ project_dir: "..." })
256
+ 1. brainstorm_start_session()
259
257
  2. brainstorm_push_screen({ html: "...", slot: "a", label: "Option A" })
260
258
  3. brainstorm_push_screen({ html: "...", slot: "b", label: "Option B" })
261
- 4. Tell user to compare and pick a preference
262
- 5. brainstorm_read_events({})
263
- 6. → Look for { type: "preference", choice: "a"|"b" }
264
- 7. brainstorm_stop_session({})
259
+ 4. brainstorm_read_events({ wait_seconds: 120 }) // returns when user picks
260
+ 5. { type: "preference", choice: "a"|"b" }
261
+ 6. brainstorm_stop_session()
265
262
  ```
266
263
 
267
264
  ### Multi-Round Brainstorming
268
265
 
269
266
  ```
270
- 1. brainstorm_start_session({ project_dir: "..." })
267
+ 1. brainstorm_start_session()
271
268
 
272
269
  // Round 1: Layout
273
270
  2. brainstorm_push_screen({ html: "...", slot: "a", label: "Grid" })
@@ -290,7 +287,7 @@ graph TD
290
287
  ### Progressive Refinement
291
288
 
292
289
  ```
293
- 1. brainstorm_start_session({ project_dir: "..." })
290
+ 1. brainstorm_start_session()
294
291
 
295
292
  // Show initial mockup
296
293
  2. brainstorm_push_screen({ html: "...v1 mockup..." })
@@ -344,10 +341,10 @@ Three input methods: file path, stdin (`-`), or inline (`--html`). Use `--slot`
344
341
  ### `events`
345
342
 
346
343
  ```
347
- brainstorm-companion events [--format json|text] [--clear]
344
+ brainstorm-companion events [--wait <seconds>] [--format json|text] [--clear]
348
345
  ```
349
346
 
350
- Returns JSON array of user interaction events. Event types: `click`, `preference`, `tab-switch`, `view-change`.
347
+ Use `--wait 120` to wait for the user's click and return it automatically. Without `--wait`, returns immediately. Never blocks other operations. Event types: `click`, `preference`, `tab-switch`, `view-change`.
351
348
 
352
349
  ### `clear`
353
350
 
@@ -377,32 +374,31 @@ Shows Session ID, URL, uptime, event count, and active slots.
377
374
  2. `push` writes HTML files to the session directory; the file watcher detects changes and broadcasts reload to the browser via WebSocket
378
375
  3. The browser auto-reloads and renders content in a themed frame with click capture on `[data-choice]` elements
379
376
  4. Click events are sent over WebSocket to the server and appended to a `.events` JSONL file
380
- 5. `events` reads the JSONL file and returns structured JSON
377
+ 5. `events` reads the JSONL file and returns structured JSON; with `wait_seconds`, it waits for the user's click and returns it automatically (non-blocking — other tools still work)
381
378
  6. Each session is fully isolated: own port, own directory, own event log
382
379
  7. Sessions are persistent — they stay alive until explicitly stopped with `stop` or `brainstorm_stop_session`
383
380
 
384
381
  ## Best Practices
385
382
 
386
- 1. **Always pass `project_dir`** to `brainstorm_start_session` avoids cross-agent conflicts
387
- 2. **Never restart to update content** — just call `brainstorm_push_screen` again; the browser auto-reloads
388
- 3. **One `brainstorm_start_session` per workflow** — it reuses the existing session automatically
389
- 4. **Push fragments, not full documents** — the frame template handles `<html>`, theming, and scroll
390
- 5. **Start with a heading** — `<h2>` describes what the user is looking at
391
- 6. **Add a `.subtitle`** — describes the decision being made
392
- 7. **One decision per screen** — don't combine unrelated choices
393
- 8. **Use slot labels** — `label` makes comparison tabs readable
394
- 9. **Use `data-choice` for interaction** — the built-in `toggleSelect` emits events automatically
395
- 10. **Tell the user to interact** — after pushing content, let them know the browser is ready
396
- 11. **Read events after user has time** — don't immediately read; wait for user to respond
397
- 12. **Clean up with `brainstorm_stop_session`**frees the port and removes temp files
383
+ 1. **Zero config** `brainstorm_start_session()` and `brainstorm-companion start` work with no arguments; sessions auto-isolate by working directory
384
+ 3. **Never restart to update content** — just call `push_screen` / `push` again; the browser auto-reloads
385
+ 4. **One start per workflow** — `start` reuses the existing session automatically
386
+ 5. **Push fragments, not full documents** — the frame template handles `<html>`, theming, and scroll
387
+ 6. **Start with a heading** — `<h2>` describes what the user is looking at
388
+ 7. **Add a `.subtitle`** — describes the decision being made
389
+ 8. **One decision per screen** — don't combine unrelated choices
390
+ 9. **Use slot labels** — `label` makes comparison tabs readable
391
+ 10. **Use `data-choice` for interaction** — the built-in `toggleSelect` emits events automatically
392
+ 11. **Tell the user to interact** — after pushing content, let them know the browser is ready
393
+ 12. **Read events after user has time** — don't immediately read; wait for user to respond
394
+ 13. **Use `--timeout <min>` for auto-cleanup** or call `stop` / `brainstorm_stop_session` when done
398
395
 
399
396
  ## Common Mistakes
400
397
 
401
398
  - **Starting a new session for each update** — DON'T. Call `push_screen` to update the existing browser.
402
- - **Omitting `project_dir`** — leads to `/tmp` collisions between agents.
403
399
  - **Pushing full HTML documents** — push fragments; the frame template adds theming and structure.
404
400
  - **Reading events immediately after push** — give the user time to interact first.
405
- - **Forgetting to stop** — always call `brainstorm_stop_session` when done.
401
+ - **Forgetting to stop** — always call `brainstorm_stop_session` / `stop` when done, or use `--timeout`.
406
402
 
407
403
  ## Author
408
404
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainstorm-companion",
3
- "version": "2.0.1",
3
+ "version": "2.0.4",
4
4
  "description": "AI-assisted visual brainstorming companion",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
package/skill/SKILL.md CHANGED
@@ -5,15 +5,16 @@ description: Visual brainstorming companion — opens a browser window for compa
5
5
 
6
6
  # Brainstorm Companion — Complete Agent Reference
7
7
 
8
- ## Quickstart (3 calls, no setup)
8
+ ## Quickstart (4 calls, no setup, user's choice comes back automatically)
9
9
 
10
10
  ```
11
11
  brainstorm_start_session()
12
- brainstorm_push_screen({ html: "<h2>Hello World</h2><p>Your content here</p>" })
12
+ brainstorm_push_screen({ html: "<h2>Pick a layout</h2><div class='options'><div class='option' data-choice='grid' onclick='toggleSelect(this)'><div class='letter'>A</div><div class='content'><h3>Grid</h3></div></div><div class='option' data-choice='list' onclick='toggleSelect(this)'><div class='letter'>B</div><div class='content'><h3>List</h3></div></div></div>" })
13
+ brainstorm_read_events({ wait_seconds: 120 }) // ← blocks until user clicks, returns their choice
13
14
  brainstorm_stop_session()
14
15
  ```
15
16
 
16
- That's it. No arguments required. A browser opens, your HTML appears, and cleanup happens automatically.
17
+ No arguments required. Browser opens, content appears, user clicks, event returns to you automatically.
17
18
 
18
19
  ## When to Use
19
20
 
@@ -53,7 +54,7 @@ brainstorm_start_session({
53
54
  })
54
55
  ```
55
56
 
56
- **`project_dir` is optional.** Without it, sessions go to `/tmp/brainstorm-companion/`. Pass the current working directory for project-local storage or when multiple agents may run simultaneously.
57
+ **No arguments required.** Sessions are auto-isolated by working directory different projects never collide, even without `project_dir`. Pass `project_dir` only if you want session files stored inside the project folder.
57
58
 
58
59
  **Calling it multiple times is safe** — returns the existing session. Just call `brainstorm_push_screen` to update content.
59
60
 
@@ -85,14 +86,22 @@ When slots are used, the browser switches to comparison mode with:
85
86
 
86
87
  ### brainstorm_read_events
87
88
 
88
- Read user interaction events (clicks, preferences).
89
+ Read user interaction events. **Use `wait_seconds` to get the user's click automatically** — returns as soon as the user interacts.
90
+
91
+ **Non-blocking:** If you call any other tool (push, clear, stop) while waiting, the wait is cancelled immediately and the new tool runs. You can always push updated content or stop the session — `wait_seconds` never blocks anything.
89
92
 
90
93
  ```
91
- brainstorm_read_events({ clear_after_read: false })
94
+ // Recommended waits for user's click, returns it automatically:
95
+ brainstorm_read_events({ wait_seconds: 120 })
96
+ → { events: [{ type: "click", choice: "grid", text: "Grid Layout" }], count: 1 }
97
+
98
+ // Immediate (returns whatever is available now):
99
+ brainstorm_read_events({})
92
100
  → { events: [...], count: N }
93
101
  ```
94
102
 
95
103
  Set `clear_after_read: true` between brainstorming rounds to avoid stale events.
104
+ If the user never clicks, returns `{ events: [], count: 0 }` after the timeout.
96
105
 
97
106
  ### brainstorm_clear_screen
98
107
 
@@ -344,43 +353,41 @@ All events include a `timestamp` field (Unix ms).
344
353
  ### Single Decision
345
354
 
346
355
  ```
347
- 1. brainstorm_start_session({ project_dir: "..." })
356
+ 1. brainstorm_start_session()
348
357
  2. brainstorm_push_screen({ html: "...options with data-choice..." })
349
- 3. Tell user to make their selection in the browser
350
- 4. brainstorm_read_events({})
351
- 5. → Use the choice to proceed
352
- 6. brainstorm_stop_session({})
358
+ 3. brainstorm_read_events({ wait_seconds: 120 }) // blocks until user clicks
359
+ 4. → User's choice arrives automatically — use it to proceed
360
+ 5. brainstorm_stop_session()
353
361
  ```
354
362
 
355
363
  ### A/B/C Comparison
356
364
 
357
365
  ```
358
- 1. brainstorm_start_session({ project_dir: "..." })
366
+ 1. brainstorm_start_session()
359
367
  2. brainstorm_push_screen({ html: "...", slot: "a", label: "Option A" })
360
368
  3. brainstorm_push_screen({ html: "...", slot: "b", label: "Option B" })
361
- 4. Tell user to compare and pick a preference
362
- 5. brainstorm_read_events({})
363
- 6. → Look for { type: "preference", choice: "a"|"b" }
364
- 7. brainstorm_stop_session({})
369
+ 4. brainstorm_read_events({ wait_seconds: 120 }) // blocks until user picks preference
370
+ 5. → Look for { type: "preference", choice: "a"|"b" }
371
+ 6. brainstorm_stop_session()
365
372
  ```
366
373
 
367
374
  ### Multi-Round Brainstorming
368
375
 
369
376
  ```
370
- 1. brainstorm_start_session({ project_dir: "..." })
377
+ 1. brainstorm_start_session()
371
378
 
372
379
  // Round 1: Layout
373
380
  2. brainstorm_push_screen({ html: "...", slot: "a", label: "Grid" })
374
381
  3. brainstorm_push_screen({ html: "...", slot: "b", label: "List" })
375
- 4. brainstorm_read_events({ clear_after_read: true })
376
- 5. → User chose "Grid"
382
+ 4. brainstorm_read_events({ wait_seconds: 120, clear_after_read: true })
383
+ 5. → User chose "Grid" (returned automatically)
377
384
 
378
385
  // Round 2: Color scheme (clear old slots first)
379
386
  6. brainstorm_clear_screen({})
380
387
  7. brainstorm_push_screen({ html: "...", slot: "a", label: "Light" })
381
388
  8. brainstorm_push_screen({ html: "...", slot: "b", label: "Dark" })
382
- 9. brainstorm_read_events({ clear_after_read: true })
383
- 10. → User chose "Dark"
389
+ 9. brainstorm_read_events({ wait_seconds: 120, clear_after_read: true })
390
+ 10. → User chose "Dark" (returned automatically)
384
391
 
385
392
  // Show final summary
386
393
  11. brainstorm_push_screen({ html: "<h2>Decisions: Grid + Dark</h2>..." })
@@ -390,7 +397,7 @@ All events include a `timestamp` field (Unix ms).
390
397
  ### Progressive Refinement
391
398
 
392
399
  ```
393
- 1. brainstorm_start_session({ project_dir: "..." })
400
+ 1. brainstorm_start_session()
394
401
 
395
402
  // Show initial mockup
396
403
  2. brainstorm_push_screen({ html: "...v1 mockup..." })
@@ -407,23 +414,32 @@ All events include a `timestamp` field (Unix ms).
407
414
 
408
415
  ## Best Practices
409
416
 
410
- 1. **Always pass `project_dir`** to `brainstorm_start_session` avoids cross-agent conflicts
411
- 2. **Never restart to update content** — just call `brainstorm_push_screen` again; the browser auto-reloads
412
- 3. **One `brainstorm_start_session` per workflow** — it reuses the existing session automatically
413
- 4. **Push fragments, not full documents** — the frame template handles `<html>`, theming, and scroll
414
- 5. **Start with a heading** — `<h2>` describes what the user is looking at
415
- 6. **Add a `.subtitle`** — describes the decision being made
416
- 7. **One decision per screen** — don't combine unrelated choices
417
- 8. **Use slot labels** — `label` makes comparison tabs readable
418
- 9. **Use `data-choice` for interaction** — the built-in `toggleSelect` emits events automatically
419
- 10. **Tell the user to interact** — after pushing content, let them know the browser is ready
420
- 11. **Read events after user has time** — don't immediately read; wait for user to respond
421
- 12. **Clean up with `brainstorm_stop_session`** — frees the port and removes temp files
417
+ 1. **Zero config** `brainstorm_start_session()` works with no arguments; isolation is automatic
418
+ 3. **Never restart to update content** — just call `brainstorm_push_screen` again; the browser auto-reloads
419
+ 4. **One `brainstorm_start_session` per workflow** — it reuses the existing session automatically
420
+ 5. **Push fragments, not full documents** — the frame template handles `<html>`, theming, and scroll
421
+ 6. **Start with a heading** — `<h2>` describes what the user is looking at
422
+ 7. **Add a `.subtitle`** — describes the decision being made
423
+ 8. **One decision per screen** — don't combine unrelated choices
424
+ 9. **Use slot labels** — `label` makes comparison tabs readable
425
+ 10. **Use `data-choice` for interaction** — the built-in `toggleSelect` emits events automatically
426
+ 11. **Tell the user to interact** — after pushing content, let them know the browser is ready
427
+ 12. **Read events after user has time** — don't immediately read; wait for user to respond
428
+ 13. **Clean up with `brainstorm_stop_session`** — or use `idle_timeout_minutes` for auto-cleanup
422
429
 
423
430
  ## Common Mistakes
424
431
 
425
432
  - **Starting a new session for each update** — DON'T. Call `push_screen` to update the existing browser.
426
- - **Omitting `project_dir`** — leads to `/tmp` collisions between agents.
427
433
  - **Pushing full HTML documents** — push fragments; the frame template adds theming and structure.
428
434
  - **Reading events immediately after push** — give the user time to interact first.
429
- - **Forgetting to stop** — always call `brainstorm_stop_session` when done.
435
+ - **Forgetting to stop** — always call `brainstorm_stop_session` when done, or use `idle_timeout_minutes`.
436
+
437
+ ---
438
+
439
+ ## Related Documentation
440
+
441
+ For deeper reference beyond this skill file:
442
+
443
+ - **[visual-companion.md](./visual-companion.md)** — Detailed usage guide: complete CSS class descriptions, all auto-detected library examples (Mermaid flowcharts/sequence/state, Prism languages, KaTeX math), event handling patterns, content design best practices, and a full multi-step brainstorming workflow walkthrough.
444
+ - **[README](https://www.npmjs.com/package/brainstorm-companion)** — Install instructions, CLI reference with all flags, MCP setup for Claude Code, parallel instance usage, and architecture overview.
445
+ - **CLI help** — Run `brainstorm-companion --help` or `brainstorm-companion <command> --help` for per-command documentation with examples and CSS class reference.
@@ -2,6 +2,8 @@
2
2
 
3
3
  This guide covers advanced usage patterns for the Brainstorm Companion tool.
4
4
 
5
+ **See also:** [SKILL.md](./SKILL.md) for the complete agent reference (quickstart, MCP tools, workflow patterns, best practices). [README](https://www.npmjs.com/package/brainstorm-companion) for install, CLI reference, and MCP setup.
6
+
5
7
  ---
6
8
 
7
9
  ## When to Use Browser vs Terminal
@@ -137,14 +139,21 @@ Inline math with `$...$`, display math with `$$...$$`:
137
139
 
138
140
  ## Event Handling Patterns
139
141
 
140
- ### Basic Polling
142
+ ### Automatic (Recommended)
141
143
 
142
- After pushing content, give the user time to interact, then read events:
144
+ Use `wait_seconds` the user's click comes back automatically. Non-blocking: you can still push content or stop the session while waiting.
143
145
 
144
146
  ```
145
147
  brainstorm_push_screen({ html: "..." })
146
- // ... wait for user to interact ...
147
- brainstorm_read_events()
148
+ brainstorm_read_events({ wait_seconds: 120 }) // returns when user clicks
149
+ ```
150
+
151
+ ### Immediate
152
+
153
+ Returns whatever events exist right now, without waiting:
154
+
155
+ ```
156
+ brainstorm_read_events({})
148
157
  ```
149
158
 
150
159
  ### Clearing Events Between Rounds
@@ -152,9 +161,9 @@ brainstorm_read_events()
152
161
  When running multi-round comparisons, clear events between rounds to avoid stale data:
153
162
 
154
163
  ```
155
- brainstorm_read_events() // read current events
156
- brainstorm_clear_screen({ target: "events" }) // clear event queue
157
- brainstorm_push_screen({ html: "Round 2..." }) // push next round
164
+ brainstorm_read_events({ wait_seconds: 120, clear_after_read: true }) // wait + clear
165
+ brainstorm_clear_screen({}) // clear content for next round
166
+ brainstorm_push_screen({ html: "Round 2..." }) // push next round
158
167
  ```
159
168
 
160
169
  ### Interpreting Events
@@ -250,10 +259,10 @@ stateDiagram-v2
250
259
 
251
260
  This example shows a full workflow for choosing a navigation pattern for a new app.
252
261
 
253
- ### Step 1: Present the question
262
+ ### Step 1: Start session and present the question
254
263
 
255
264
  ```
256
- brainstorm_start_session()
265
+ brainstorm_start_session() // no args needed — opens browser automatically
257
266
 
258
267
  brainstorm_push_screen({
259
268
  html: `
package/src/cli.js CHANGED
@@ -122,6 +122,7 @@ Read user interaction events from the active brainstorm session.
122
122
  Events are generated when users click interactive elements in the browser.
123
123
 
124
124
  Options:
125
+ --wait <seconds> Wait for an event to arrive (returns immediately when one does)
125
126
  --format <json|text> Output format (default: json)
126
127
  --clear Clear events after reading
127
128
  --project-dir <path> Session storage location
@@ -134,9 +135,9 @@ Event Types:
134
135
  view-change User toggled view mode in comparison mode
135
136
 
136
137
  Examples:
137
- brainstorm-companion events
138
- brainstorm-companion events --format text --clear
139
- brainstorm-companion events --session 1234-567890`,
138
+ brainstorm-companion events --wait 120 # wait for user click, return it
139
+ brainstorm-companion events # return events immediately
140
+ brainstorm-companion events --format text --clear`,
140
141
 
141
142
  clear: `Usage: brainstorm-companion clear [options]
142
143
 
@@ -185,6 +186,18 @@ function printHelp(command) {
185
186
  // Helpers
186
187
  // ---------------------------------------------------------------------------
187
188
 
189
+ function printNextSteps() {
190
+ console.log(`
191
+ Next steps:
192
+ brainstorm-companion push --html '<h2>Your content</h2>' Push content to browser
193
+ brainstorm-companion push file.html --slot a --label "A" Comparison mode
194
+ brainstorm-companion events Read user interactions
195
+ brainstorm-companion stop Stop when done
196
+
197
+ Full docs: https://www.npmjs.com/package/brainstorm-companion
198
+ Run: brainstorm-companion push --help for all CSS classes and options`);
199
+ }
200
+
188
201
  function sleep(ms) {
189
202
  return new Promise(resolve => setTimeout(resolve, ms));
190
203
  }
@@ -272,6 +285,7 @@ async function start(argv) {
272
285
  const url = existing.serverInfo.url;
273
286
  console.log(`Server already running: ${url}`);
274
287
  console.log(`Session ID: ${existing.sessionId}`);
288
+ printNextSteps();
275
289
  if (!noOpen) {
276
290
  openBrowser(url);
277
291
  }
@@ -294,6 +308,7 @@ async function start(argv) {
294
308
  instance.server.once('listening', () => {
295
309
  console.log(`Server started: ${instance.url}`);
296
310
  console.log(`Session ID: ${path.basename(sessionDir)}`);
311
+ printNextSteps();
297
312
  if (!noOpen) {
298
313
  openBrowser(instance.url);
299
314
  }
@@ -338,6 +353,7 @@ async function start(argv) {
338
353
 
339
354
  console.log(`Server started: ${serverInfo.url}`);
340
355
  console.log(`Session ID: ${path.basename(sessionDir)}`);
356
+ printNextSteps();
341
357
 
342
358
  if (!noOpen) {
343
359
  openBrowser(serverInfo.url);
@@ -399,12 +415,13 @@ async function push(argv) {
399
415
  // Command: events
400
416
  // ---------------------------------------------------------------------------
401
417
 
402
- function events(argv) {
418
+ async function events(argv) {
403
419
  const { values } = parseArgs({
404
420
  args: argv,
405
421
  options: {
406
422
  'project-dir': { type: 'string' },
407
423
  'session': { type: 'string' },
424
+ 'wait': { type: 'string' },
408
425
  'format': { type: 'string', default: 'json' },
409
426
  'clear': { type: 'boolean', default: false },
410
427
  },
@@ -413,11 +430,28 @@ function events(argv) {
413
430
 
414
431
  const projectDir = values['project-dir'] || null;
415
432
  const sessionId = values['session'] || null;
433
+ const waitSeconds = values['wait'] ? parseInt(values['wait'], 10) : 0;
416
434
  const format = values['format'];
417
435
  const doClear = values['clear'];
418
436
 
419
- const { session } = getActiveOrExit(projectDir, sessionId);
420
- const eventList = session.readEvents();
437
+ const { session, active } = getActiveOrExit(projectDir, sessionId);
438
+
439
+ let eventList;
440
+ if (waitSeconds > 0) {
441
+ // Poll until events arrive or timeout
442
+ const eventsPath = path.join(active.sessionDir, '.events');
443
+ const deadlineMs = waitSeconds * 1000;
444
+ const pollMs = 500;
445
+ const startTime = Date.now();
446
+ eventList = [];
447
+ while (Date.now() - startTime < deadlineMs) {
448
+ eventList = session.readEvents();
449
+ if (eventList.length > 0) break;
450
+ await sleep(pollMs);
451
+ }
452
+ } else {
453
+ eventList = session.readEvents();
454
+ }
421
455
 
422
456
  if (format === 'text') {
423
457
  if (eventList.length === 0) {
@@ -429,7 +463,6 @@ function events(argv) {
429
463
  }
430
464
  }
431
465
  } else {
432
- // Default: json
433
466
  console.log(JSON.stringify(eventList, null, 2));
434
467
  }
435
468
 
package/src/mcp.js CHANGED
@@ -1,16 +1,22 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('node:crypto');
3
4
  const fs = require('node:fs');
4
5
  const path = require('node:path');
5
6
  const { exec } = require('node:child_process');
6
7
  const { startServer } = require('./server');
7
8
 
9
+ function cwdHash() {
10
+ return crypto.createHash('md5').update(process.cwd()).digest('hex').slice(0, 8);
11
+ }
12
+
8
13
  class McpServer {
9
14
  constructor() {
10
15
  this.sessionDir = null; // absolute path once session is started
11
16
  this.serverInstance = null; // startServer() result
12
17
  this.buffer = ''; // stdin buffer
13
18
  this._pending = null; // Promise of the in-flight tool call (for serialization)
19
+ this._cancelWait = null; // Cancel function for pending read_events wait
14
20
  }
15
21
 
16
22
  start() {
@@ -75,51 +81,60 @@ class McpServer {
75
81
  }
76
82
 
77
83
  handleToolCall(id, params) {
78
- // Serialize tool calls: each waits for the previous to finish. This ensures
79
- // that brainstorm_start_session (async) responds before subsequent tools run.
80
- const prev = this._pending || Promise.resolve();
81
- const next = prev.then(() => {
82
- const { name, arguments: args = {} } = params || {};
83
- let resultOrPromise;
84
- try {
85
- switch (name) {
86
- case 'brainstorm_start_session': resultOrPromise = this.toolStartSession(args); break;
87
- case 'brainstorm_push_screen': resultOrPromise = this.toolPushScreen(args); break;
88
- case 'brainstorm_read_events': resultOrPromise = this.toolReadEvents(args); break;
89
- case 'brainstorm_clear_screen': resultOrPromise = this.toolClearScreen(args); break;
90
- case 'brainstorm_stop_session': resultOrPromise = this.toolStopSession(args); break;
91
- default:
92
- this.respondError(id, -32602, `Unknown tool: ${name}`);
93
- return;
94
- }
95
- } catch (err) {
96
- this.respond(id, {
97
- content: [{ type: 'text', text: `Error: ${err.message}` }],
98
- isError: true
99
- });
100
- return;
101
- }
84
+ const { name, arguments: args = {} } = params || {};
102
85
 
103
- const sendResult = (result) => {
104
- this.respond(id, {
105
- content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }]
106
- });
107
- };
108
- const sendError = (err) => {
109
- this.respond(id, {
110
- content: [{ type: 'text', text: `Error: ${err.message}` }],
111
- isError: true
112
- });
113
- };
86
+ // Cancel any pending read_events wait so it doesn't block new calls
87
+ if (name !== 'brainstorm_read_events' && this._cancelWait) {
88
+ this._cancelWait();
89
+ this._cancelWait = null;
90
+ }
114
91
 
115
- if (resultOrPromise && typeof resultOrPromise.then === 'function') {
116
- return resultOrPromise.then(sendResult).catch(sendError);
92
+ const sendResult = (result) => {
93
+ this.respond(id, {
94
+ content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }]
95
+ });
96
+ };
97
+ const sendError = (err) => {
98
+ this.respond(id, {
99
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
100
+ isError: true
101
+ });
102
+ };
103
+
104
+ // Start session is async (waits for 'listening') — serialize it
105
+ if (name === 'brainstorm_start_session') {
106
+ const prev = this._pending || Promise.resolve();
107
+ const next = prev.then(() => {
108
+ try {
109
+ const result = this.toolStartSession(args);
110
+ if (result && typeof result.then === 'function') {
111
+ return result.then(sendResult).catch(sendError);
112
+ }
113
+ sendResult(result);
114
+ } catch (err) { sendError(err); }
115
+ });
116
+ this._pending = next.catch(() => {});
117
+ return;
118
+ }
119
+
120
+ // All other tools run immediately (no serialization)
121
+ try {
122
+ let result;
123
+ switch (name) {
124
+ case 'brainstorm_push_screen': result = this.toolPushScreen(args); break;
125
+ case 'brainstorm_read_events': result = this.toolReadEvents(args); break;
126
+ case 'brainstorm_clear_screen': result = this.toolClearScreen(args); break;
127
+ case 'brainstorm_stop_session': result = this.toolStopSession(args); break;
128
+ default:
129
+ this.respondError(id, -32602, `Unknown tool: ${name}`);
130
+ return;
131
+ }
132
+ if (result && typeof result.then === 'function') {
133
+ result.then(sendResult).catch(sendError);
117
134
  } else {
118
- sendResult(resultOrPromise);
135
+ sendResult(result);
119
136
  }
120
- });
121
- // Track the tail of the chain; errors in earlier steps shouldn't block later ones
122
- this._pending = next.catch(() => {});
137
+ } catch (err) { sendError(err); }
123
138
  }
124
139
 
125
140
  getToolDefinitions() {
@@ -128,19 +143,21 @@ class McpServer {
128
143
  name: 'brainstorm_start_session',
129
144
  description: `Start a visual brainstorming session. Opens a browser window where you push HTML and users interact visually.
130
145
 
131
- QUICKSTART — just these 3 calls:
132
- brainstorm_start_session() → opens browser
133
- brainstorm_push_screen({ html: "<h2>Hello</h2><p>Content</p>" }) → shows content
134
- brainstorm_stop_session() cleans up
146
+ QUICKSTART — show content and get user's choice:
147
+ brainstorm_start_session()
148
+ brainstorm_push_screen({ html: "<h2>Pick one</h2>..." })
149
+ brainstorm_read_events({ wait_seconds: 120 }) blocks until user clicks, returns choice
150
+ brainstorm_stop_session()
135
151
 
136
152
  FULL WORKFLOW:
137
- 1. Call brainstorm_start_session ONCE (no args required — works immediately). Returns { url, session_dir }.
138
- 2. Call brainstorm_push_screen with HTML — browser auto-reloads. Call as many times as needed.
139
- 3. Call brainstorm_read_events to get user clicks/preferences.
140
- 4. Call brainstorm_stop_session when done.
153
+ 1. brainstorm_start_session() no args needed. Returns { url, session_dir }.
154
+ 2. brainstorm_push_screen({ html }) — browser auto-reloads. Call as many times as needed.
155
+ 3. brainstorm_read_events({ wait_seconds: 120 }) — BLOCKS until user interacts, then returns events automatically. No polling needed.
156
+ 4. brainstorm_stop_session() clean up.
157
+
158
+ KEY: Use wait_seconds in read_events so the user's click comes back to you automatically. No need to ask the user "what did you pick?" — the event arrives on its own.
141
159
 
142
- If a session is already running, this returns the existing URL (safe to call repeatedly).
143
- Sessions persist until explicitly stopped — no timeout by default.
160
+ Safe to call start repeatedly reuses existing session. Sessions persist until stopped.
144
161
 
145
162
  COMPARISON MODE: Push to slots a/b/c with labels for side-by-side view:
146
163
  brainstorm_push_screen({ html: "...", slot: "a", label: "Option A" })
@@ -162,15 +179,15 @@ AUTO-DETECTED (CDN injected): class="mermaid" (diagrams), class="language-*" (sy
162
179
  EVENTS: click (choice,text), preference (choice), tab-switch (slot), view-change (mode)
163
180
 
164
181
  RULES:
182
+ - Use wait_seconds in read_events — the user's choice comes back automatically
165
183
  - NEVER restart to update — just push_screen again
166
184
  - Push HTML fragments, not full <html> documents
167
185
  - Tell user the browser is ready after pushing
168
- - Give user time before reading events
169
186
  - Always stop_session when done`,
170
187
  inputSchema: {
171
188
  type: 'object',
172
189
  properties: {
173
- project_dir: { type: 'string', description: 'Project directory for session storage. Optional — defaults to /tmp/brainstorm-companion/. Pass cwd for project-local storage.' },
190
+ project_dir: { type: 'string', description: 'Optional. Stores session files under <dir>/.superpowers/brainstorm/. If omitted, auto-isolates by working directory.' },
174
191
  port: { type: 'number', description: 'Port to bind to (default: random ephemeral)' },
175
192
  open_browser: { type: 'boolean', description: 'Open browser automatically (default: true)' },
176
193
  idle_timeout_minutes: { type: 'number', description: 'Auto-stop after N minutes idle (default: 0 = no timeout)' }
@@ -203,14 +220,16 @@ Auto-detected: class="mermaid" (diagrams), class="language-*" (syntax highlighti
203
220
  name: 'brainstorm_read_events',
204
221
  description: `Read user interaction events from the brainstorm browser. Returns { events: [...], count: N }.
205
222
 
223
+ RECOMMENDED: Use wait_seconds (e.g. 120) to block until the user clicks something. This way you get the result automatically — no need to poll or ask the user to confirm.
224
+
206
225
  Event types: click (data-choice element clicked — fields: choice, text, id), preference (slot comparison pick — fields: choice), tab-switch (tab changed — fields: slot), view-change (view toggled — fields: mode). All events include timestamp.
207
226
 
208
- Use clear_after_read: true between brainstorming rounds to avoid reading stale events from the previous round.
209
- Give the user time to interact before reading — don't read immediately after pushing content.`,
227
+ Use clear_after_read: true between brainstorming rounds to avoid stale events.`,
210
228
  inputSchema: {
211
229
  type: 'object',
212
230
  properties: {
213
- clear_after_read: { type: 'boolean', description: 'Clear events after reading to avoid stale data in next round (default: false)' }
231
+ wait_seconds: { type: 'number', description: 'Wait up to N seconds for an event to arrive before returning. Recommended: 120. If 0 or omitted, returns immediately.' },
232
+ clear_after_read: { type: 'boolean', description: 'Clear events after reading (default: false)' }
214
233
  }
215
234
  }
216
235
  },
@@ -247,7 +266,7 @@ Give the user time to interact before reading — don't read immediately after p
247
266
  // Determine base directory and create session dir
248
267
  const baseDir = project_dir
249
268
  ? path.join(project_dir, '.superpowers', 'brainstorm')
250
- : '/tmp/brainstorm-companion';
269
+ : path.join('/tmp', 'brainstorm-companion', cwdHash());
251
270
  const sessionId = `${process.pid}-${Date.now()}`;
252
271
  const sessionDir = path.join(baseDir, sessionId);
253
272
  fs.mkdirSync(sessionDir, { recursive: true });
@@ -314,24 +333,63 @@ Give the user time to interact before reading — don't read immediately after p
314
333
  if (!this.sessionDir) {
315
334
  return { events: [], count: 0 };
316
335
  }
317
- const { clear_after_read = false } = args;
336
+ const { wait_seconds = 0, clear_after_read = false } = args;
318
337
  const eventsPath = path.join(this.sessionDir, '.events');
319
- let events = [];
320
- if (fs.existsSync(eventsPath)) {
338
+
339
+ const readEvents = () => {
340
+ if (!fs.existsSync(eventsPath)) return [];
321
341
  try {
322
342
  const raw = fs.readFileSync(eventsPath, 'utf8');
323
- events = raw
324
- .split('\n')
325
- .filter(line => line.trim())
326
- .map(line => JSON.parse(line));
343
+ return raw.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
327
344
  } catch {
328
- events = [];
345
+ return [];
329
346
  }
347
+ };
348
+
349
+ const finish = (events) => {
350
+ if (clear_after_read) {
351
+ try { fs.writeFileSync(eventsPath, '', 'utf8'); } catch { /* ignore */ }
352
+ }
353
+ return { events, count: events.length };
354
+ };
355
+
356
+ // Immediate mode
357
+ if (!wait_seconds || wait_seconds <= 0) {
358
+ return finish(readEvents());
330
359
  }
331
- if (clear_after_read) {
332
- try { fs.writeFileSync(eventsPath, '', 'utf8'); } catch { /* ignore */ }
333
- }
334
- return { events, count: events.length };
360
+
361
+ // Wait mode poll every 500ms until events arrive, cancelled, or timeout
362
+ const deadlineMs = wait_seconds * 1000;
363
+ const pollMs = 500;
364
+ return new Promise((resolve) => {
365
+ let cancelled = false;
366
+ let timer = null;
367
+
368
+ // Register cancel so other tool calls can interrupt this wait
369
+ this._cancelWait = () => {
370
+ cancelled = true;
371
+ if (timer) clearTimeout(timer);
372
+ resolve(finish(readEvents())); // return whatever we have so far
373
+ };
374
+
375
+ const startTime = Date.now();
376
+ const check = () => {
377
+ if (cancelled) return;
378
+ const events = readEvents();
379
+ if (events.length > 0) {
380
+ this._cancelWait = null;
381
+ resolve(finish(events));
382
+ return;
383
+ }
384
+ if (Date.now() - startTime >= deadlineMs) {
385
+ this._cancelWait = null;
386
+ resolve(finish([]));
387
+ return;
388
+ }
389
+ timer = setTimeout(check, pollMs);
390
+ };
391
+ check();
392
+ });
335
393
  }
336
394
 
337
395
  toolClearScreen(args) {
package/src/session.js CHANGED
@@ -1,13 +1,18 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('node:crypto');
3
4
  const fs = require('node:fs');
4
5
  const path = require('node:path');
5
6
 
7
+ function cwdHash() {
8
+ return crypto.createHash('md5').update(process.cwd()).digest('hex').slice(0, 8);
9
+ }
10
+
6
11
  class SessionManager {
7
12
  constructor(projectDir, targetSessionId) {
8
13
  this.baseDir = projectDir
9
- ? `${projectDir}/.superpowers/brainstorm/`
10
- : `/tmp/brainstorm-companion/`;
14
+ ? path.join(projectDir, '.superpowers', 'brainstorm')
15
+ : path.join('/tmp', 'brainstorm-companion', cwdHash());
11
16
  this.targetSessionId = targetSessionId || null;
12
17
  }
13
18