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 +30 -34
- package/package.json +1 -1
- package/skill/SKILL.md +52 -36
- package/skill/visual-companion.md +18 -9
- package/src/cli.js +40 -7
- package/src/mcp.js +127 -69
- package/src/session.js +7 -2
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
|
|
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.**
|
|
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.
|
|
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
|
|
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(
|
|
246
|
+
1. brainstorm_start_session()
|
|
248
247
|
2. brainstorm_push_screen({ html: "...options with data-choice..." })
|
|
249
|
-
3.
|
|
250
|
-
4.
|
|
251
|
-
5.
|
|
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(
|
|
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.
|
|
262
|
-
5.
|
|
263
|
-
6.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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. **
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
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 (
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
356
|
+
1. brainstorm_start_session()
|
|
348
357
|
2. brainstorm_push_screen({ html: "...options with data-choice..." })
|
|
349
|
-
3.
|
|
350
|
-
4.
|
|
351
|
-
5.
|
|
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(
|
|
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.
|
|
362
|
-
5.
|
|
363
|
-
6.
|
|
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(
|
|
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(
|
|
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. **
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
###
|
|
142
|
+
### Automatic (Recommended)
|
|
141
143
|
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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()
|
|
156
|
-
brainstorm_clear_screen({
|
|
157
|
-
brainstorm_push_screen({ html: "Round 2..." })
|
|
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:
|
|
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
|
|
139
|
-
brainstorm-companion events --
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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(
|
|
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 —
|
|
132
|
-
brainstorm_start_session()
|
|
133
|
-
brainstorm_push_screen({ html: "<h2>
|
|
134
|
-
|
|
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.
|
|
138
|
-
2.
|
|
139
|
-
3.
|
|
140
|
-
4.
|
|
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
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
338
|
+
|
|
339
|
+
const readEvents = () => {
|
|
340
|
+
if (!fs.existsSync(eventsPath)) return [];
|
|
321
341
|
try {
|
|
322
342
|
const raw = fs.readFileSync(eventsPath, 'utf8');
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
?
|
|
10
|
-
:
|
|
14
|
+
? path.join(projectDir, '.superpowers', 'brainstorm')
|
|
15
|
+
: path.join('/tmp', 'brainstorm-companion', cwdHash());
|
|
11
16
|
this.targetSessionId = targetSessionId || null;
|
|
12
17
|
}
|
|
13
18
|
|