bitwrench 2.0.21 → 2.0.23

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.
Files changed (88) hide show
  1. package/LICENSE.txt +1 -1
  2. package/README.md +4 -5
  3. package/bin/bwmcp.js +3 -0
  4. package/dist/bitwrench-bccl.cjs.js +1 -1
  5. package/dist/bitwrench-bccl.cjs.min.js +1 -1
  6. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  7. package/dist/bitwrench-bccl.esm.js +1 -1
  8. package/dist/bitwrench-bccl.esm.min.js +1 -1
  9. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  10. package/dist/bitwrench-bccl.umd.js +1 -1
  11. package/dist/bitwrench-bccl.umd.min.js +1 -1
  12. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  13. package/dist/bitwrench-code-edit.cjs.js +1 -1
  14. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  15. package/dist/bitwrench-code-edit.es5.js +1 -1
  16. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  17. package/dist/bitwrench-code-edit.esm.js +1 -1
  18. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  19. package/dist/bitwrench-code-edit.umd.js +1 -1
  20. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  21. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  22. package/dist/bitwrench-debug.js +1 -1
  23. package/dist/bitwrench-debug.min.js +1 -1
  24. package/dist/bitwrench-lean.cjs.js +3 -3
  25. package/dist/bitwrench-lean.cjs.min.js +2 -2
  26. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  27. package/dist/bitwrench-lean.es5.js +3 -3
  28. package/dist/bitwrench-lean.es5.min.js +2 -2
  29. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  30. package/dist/bitwrench-lean.esm.js +3 -3
  31. package/dist/bitwrench-lean.esm.min.js +2 -2
  32. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  33. package/dist/bitwrench-lean.umd.js +3 -3
  34. package/dist/bitwrench-lean.umd.min.js +2 -2
  35. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  36. package/dist/bitwrench-util-css.cjs.js +1 -1
  37. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  38. package/dist/bitwrench-util-css.es5.js +1 -1
  39. package/dist/bitwrench-util-css.es5.min.js +1 -1
  40. package/dist/bitwrench-util-css.esm.js +1 -1
  41. package/dist/bitwrench-util-css.esm.min.js +1 -1
  42. package/dist/bitwrench-util-css.umd.js +1 -1
  43. package/dist/bitwrench-util-css.umd.min.js +1 -1
  44. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  45. package/dist/bitwrench.cjs.js +3 -3
  46. package/dist/bitwrench.cjs.min.js +2 -2
  47. package/dist/bitwrench.cjs.min.js.gz +0 -0
  48. package/dist/bitwrench.css +1 -1
  49. package/dist/bitwrench.es5.js +3 -3
  50. package/dist/bitwrench.es5.min.js +2 -2
  51. package/dist/bitwrench.es5.min.js.gz +0 -0
  52. package/dist/bitwrench.esm.js +3 -3
  53. package/dist/bitwrench.esm.min.js +2 -2
  54. package/dist/bitwrench.esm.min.js.gz +0 -0
  55. package/dist/bitwrench.umd.js +3 -3
  56. package/dist/bitwrench.umd.min.js +2 -2
  57. package/dist/bitwrench.umd.min.js.gz +0 -0
  58. package/dist/builds.json +61 -61
  59. package/dist/bwserve.cjs.js +2 -2
  60. package/dist/bwserve.esm.js +2 -2
  61. package/dist/sri.json +45 -45
  62. package/docs/README.md +76 -0
  63. package/docs/app-patterns.md +264 -0
  64. package/docs/bitwrench-mcp.md +426 -0
  65. package/docs/bitwrench_api.md +2232 -0
  66. package/docs/bw-attach.md +399 -0
  67. package/docs/bwserve.md +841 -0
  68. package/docs/cli.md +307 -0
  69. package/docs/component-cheatsheet.md +144 -0
  70. package/docs/component-library.md +1099 -0
  71. package/docs/framework-translation-table.md +33 -0
  72. package/docs/llm-bitwrench-guide.md +672 -0
  73. package/docs/routing.md +562 -0
  74. package/docs/state-management.md +767 -0
  75. package/docs/taco-format.md +373 -0
  76. package/docs/theming.md +309 -0
  77. package/docs/thinking-in-bitwrench.md +1457 -0
  78. package/docs/tutorial-bwserve.md +297 -0
  79. package/docs/tutorial-embedded.md +314 -0
  80. package/docs/tutorial-website.md +255 -0
  81. package/package.json +11 -3
  82. package/readme.html +6 -5
  83. package/src/mcp/knowledge.js +231 -0
  84. package/src/mcp/live.js +226 -0
  85. package/src/mcp/server.js +216 -0
  86. package/src/mcp/tools.js +369 -0
  87. package/src/mcp/transport.js +55 -0
  88. package/src/version.js +3 -3
@@ -0,0 +1,841 @@
1
+ # bwserve — Server-Driven UI for Bitwrench
2
+
3
+ ## What is bwserve?
4
+
5
+ bwserve is a server-side library that lets you build interactive web UIs entirely from Node.js. Your application state lives on the server, and the server pushes rendering commands to the browser over SSE (Server-Sent Events). User interactions are sent back as actions via HTTP POST.
6
+
7
+ This is the same pattern as Streamlit (Python), Phoenix LiveView (Elixir), and htmx — but bwserve sends TACO objects (bitwrench's {t, a, c, o} format), not HTML strings. The browser already has bitwrench loaded (~40KB), so it renders TACO natively with no build step.
8
+
9
+ **Key characteristics:**
10
+ - Zero runtime dependencies — only Node.js stdlib (`http`, `fs`, `path`)
11
+ - Auto-generates the client page (loads bitwrench, opens SSE, wires actions)
12
+ - Same protocol works for Node.js servers and ESP32/Arduino embedded devices
13
+ - 9 protocol message types covering DOM operations and remote execution
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ Browser Node.js Server
19
+ ┌──────────────────┐ ┌──────────────────────┐
20
+ │ bitwrench.js │ │ bwserve │
21
+ │ │ GET / │ │
22
+ │ bwclient.js ├──────────────> app.page('/', fn) │
23
+ │ (opens SSE) │ │ │
24
+ │ │<──SSE──────── │ DOM operations: │
25
+ │ bw.apply() │ {type,node} │ client.render() │
26
+ │ -> bw.DOM() │ │ client.patch() │
27
+ │ -> bw.patch() │ │ client.append() │
28
+ │ │ │ client.remove() │
29
+ │ │ │ client.batch() │
30
+ │ │ │ │
31
+ │ │ │ Execution: │
32
+ │ │ │ client.register() │
33
+ │ │ │ client.call() │
34
+ │ │ │ client.exec() │
35
+ │ │ │ │
36
+ │ data-bw-action │ │ │
37
+ │ conn.sendAction │──POST───────> │ client.on(action,fn) │
38
+ │ │ {action,data}│ │
39
+ └──────────────────┘ └──────────────────────┘
40
+ ```
41
+
42
+ **Flow:**
43
+ 1. Browser requests page → server returns auto-generated HTML shell
44
+ 2. Shell loads bitwrench from `/bw/lib/bitwrench.umd.js` and opens SSE
45
+ 3. SSE triggers page handler → server sends TACO rendering commands
46
+ 4. Browser applies each message via `bw.apply()` → DOM updates
47
+ 5. User clicks → `data-bw-action` triggers POST → server handler runs
48
+ 6. Server sends more messages → browser updates. Loop continues.
49
+
50
+ ## Quick Start
51
+
52
+ ```javascript
53
+ // server.js
54
+ import bwserve from 'bitwrench/bwserve';
55
+
56
+ var app = bwserve.create({ port: 7902 });
57
+ var count = 0;
58
+
59
+ app.page('/', function(client) {
60
+ // 1. Render the initial UI as a TACO tree
61
+ client.render('#app', {
62
+ t: 'div', a: { style: 'padding:24px' }, c: [
63
+ { t: 'h1', c: 'Counter' },
64
+ { t: 'p', c: [
65
+ 'Count: ',
66
+ { t: 'span', a: { id: 'count', style: 'font-weight:bold' }, c: '0' }
67
+ ]},
68
+ { t: 'button', a: { 'data-bw-action': 'increment', class: 'bw-btn bw-btn-primary' }, c: '+1' },
69
+ { t: 'button', a: { 'data-bw-action': 'reset', class: 'bw-btn bw-btn-outline-secondary' }, c: 'Reset' }
70
+ ]
71
+ });
72
+
73
+ // 2. Handle user actions
74
+ client.on('increment', function() {
75
+ count++;
76
+ client.patch('count', String(count));
77
+ });
78
+
79
+ client.on('reset', function() {
80
+ count = 0;
81
+ client.patch('count', '0');
82
+ });
83
+ });
84
+
85
+ app.listen(function() {
86
+ console.log('bwserve running on http://localhost:7902');
87
+ });
88
+ ```
89
+
90
+ Save as `server.js`, run `node server.js`, open `http://localhost:7902`.
91
+
92
+ ## Protocol Messages
93
+
94
+ bwserve uses 9 message types, organized in two categories:
95
+
96
+ ### DOM Operations (5 types)
97
+
98
+ These modify the browser's DOM tree:
99
+
100
+ | Type | Purpose | Server Method | Client Action |
101
+ |------|---------|---------------|---------------|
102
+ | `replace` | Replace element content | `client.render(target, taco)` | `bw.DOM(target, node)` |
103
+ | `patch` | Update text/attributes | `client.patch(id, content, attr?)` | `bw.patch(target, content, attr)` |
104
+ | `append` | Add child element | `client.append(target, taco)` | `target.appendChild(bw.createDOM(node))` |
105
+ | `remove` | Remove element | `client.remove(target)` | `bw.cleanup(el); el.remove()` |
106
+ | `batch` | Multiple operations | `client.batch(ops)` | Execute each op in sequence |
107
+
108
+ ### Execution Operations (3 types)
109
+
110
+ These invoke functions or execute code on the client:
111
+
112
+ | Type | Purpose | Server Method | Client Action |
113
+ |------|---------|---------------|---------------|
114
+ | `register` | Send named function | `client.register(name, body)` | Store in `bw._clientFunctions` |
115
+ | `call` | Invoke function by name | `client.call(name, ...args)` | Call registered or built-in function |
116
+ | `exec` | Run arbitrary JS | `client.exec(code)` | `new Function(code)()` (needs opt-in) |
117
+
118
+ ### Additional
119
+
120
+ | Type | Purpose | Server Method | Client Action |
121
+ |------|---------|---------------|---------------|
122
+ | `message` | Component dispatch | `client.message(target, action, data)` | `bw.message(target, action, data)` |
123
+
124
+ ### Message Schemas
125
+
126
+ ```json
127
+ // --- DOM Operations ---
128
+
129
+ // replace — full subtree replacement
130
+ { "type": "replace", "target": "#app", "node": {"t":"div","c":"Hello"} }
131
+
132
+ // patch — lightweight text/attribute update
133
+ { "type": "patch", "target": "counter", "content": "42" }
134
+ { "type": "patch", "target": "status", "content": "active", "attr": {"class": "done"} }
135
+
136
+ // append — add a child
137
+ { "type": "append", "target": "#list", "node": {"t":"li","c":"New item"} }
138
+
139
+ // remove — delete from DOM
140
+ { "type": "remove", "target": "#old-item" }
141
+
142
+ // batch — multi-update
143
+ { "type": "batch", "ops": [
144
+ { "type": "patch", "target": "a", "content": "1" },
145
+ { "type": "patch", "target": "b", "content": "2" }
146
+ ]}
147
+
148
+ // --- Execution Operations ---
149
+
150
+ // register — send a named function to the client
151
+ { "type": "register", "name": "autoScroll", "body": "function(sel) { var el = document.querySelector(sel); if (el) el.scrollTop = el.scrollHeight; }" }
152
+
153
+ // call — invoke a registered or built-in function
154
+ { "type": "call", "name": "autoScroll", "args": ["#chat"] }
155
+ { "type": "call", "name": "focus", "args": ["#search-input"] }
156
+ { "type": "call", "name": "download", "args": ["report.csv", "id,name\n1,Alice", "text/csv"] }
157
+
158
+ // exec — execute arbitrary JS (requires allowExec on client)
159
+ { "type": "exec", "code": "document.title = 'Updated'" }
160
+ ```
161
+
162
+ ### Target Resolution
163
+
164
+ All DOM operation targets are resolved using:
165
+
166
+ | Pattern | Resolution | Example |
167
+ |---------|-----------|---------|
168
+ | `#selector` | CSS selector via `querySelector` | `#app`, `#counter` |
169
+ | `.selector` | CSS class selector | `.bw-card` |
170
+ | `bare-string` | `getElementById`, then `bw._el()` fallback | `counter` |
171
+
172
+ **Best practice:** Use simple `id` attributes for patchable elements:
173
+ ```javascript
174
+ // Server sends render with id:
175
+ client.render('#app', { t: 'span', a: { id: 'count' }, c: '0' });
176
+
177
+ // Later, server patches by id:
178
+ client.patch('count', '42');
179
+ ```
180
+
181
+ ### Actions (Client → Server)
182
+
183
+ User interactions flow from client to server via HTTP POST:
184
+
185
+ ```
186
+ POST /bw/return/action/:clientId
187
+ Content-Type: application/json
188
+
189
+ { "action": "increment", "data": { "inputValue": "hello" } }
190
+ ```
191
+
192
+ **Wiring actions** — add `data-bw-action` to any element:
193
+
194
+ ```javascript
195
+ // Server sends this TACO:
196
+ { t: 'button', a: { 'data-bw-action': 'save', class: 'bw-btn' }, c: 'Save' }
197
+
198
+ // When clicked, client auto-POSTs:
199
+ { action: 'save', data: {} }
200
+
201
+ // Server handles it:
202
+ client.on('save', function(data) {
203
+ client.patch('status', 'Saved!');
204
+ });
205
+ ```
206
+
207
+ If a text `<input>` is near the clicked button, its value is automatically included as `inputValue` and the input is cleared.
208
+
209
+ ## Server API Reference
210
+
211
+ ### `bwserve.create(opts)`
212
+
213
+ Create a bwserve application.
214
+
215
+ | Option | Type | Default | Description |
216
+ |--------|------|---------|-------------|
217
+ | `port` | number | 7902 | Listen port |
218
+ | `title` | string | `'bwserve'` | HTML `<title>` |
219
+ | `static` | string | null | Static file directory |
220
+ | `theme` | string/object | null | Theme preset name or config |
221
+ | `injectBitwrench` | boolean | true | Auto-inject bitwrench UMD + CSS |
222
+ | `allowExec` | boolean | false | Enable `exec` messages on client (see warning below) |
223
+ | `allowScreenshot` | boolean | false | Enable `client.screenshot()` capability |
224
+ | `keepAliveInterval` | number | 15000 | SSE keep-alive interval in ms |
225
+
226
+ > **Start without `allowExec`.** The `register/call` pattern (Tier 1 + Tier 2) handles 95% of use cases — send named functions once, invoke them by name with safe argument passing. Only enable `allowExec: true` if you genuinely need to evaluate arbitrary code strings on the client. When in doubt, leave it off.
227
+
228
+ ### `app.page(path, handler)`
229
+
230
+ Register a page handler. The `handler` function is called with a `BwServeClient` when a browser connects via SSE.
231
+
232
+ ```javascript
233
+ app.page('/', function(client) {
234
+ client.render('#app', { t: 'div', c: 'Hello' });
235
+ });
236
+
237
+ app.page('/dashboard', function(client) {
238
+ // different page, different handler
239
+ });
240
+ ```
241
+
242
+ ### `app.listen(callback?)` / `app.close()`
243
+
244
+ Start and stop the server. Both return Promises.
245
+
246
+ ```javascript
247
+ await app.listen(function() { console.log('Ready'); });
248
+ await app.close();
249
+ ```
250
+
251
+ ### `app.clientCount`
252
+
253
+ Number of active SSE connections (read-only property).
254
+
255
+ ### `app.broadcast(msg)`
256
+
257
+ Send a protocol message to all connected clients. Useful for dashboards, notifications, and multi-user apps. Returns the number of clients the message was sent to.
258
+
259
+ ```javascript
260
+ // Broadcast a patch to all browsers:
261
+ app.broadcast({ type: 'patch', target: 'status', content: 'System OK' });
262
+
263
+ // Target a specific client by setting clientId:
264
+ app.broadcast({ type: 'patch', target: 'msg', content: 'Hello', clientId: 'c1' });
265
+
266
+ // Broadcast a batch update:
267
+ app.broadcast({
268
+ type: 'batch', ops: [
269
+ { type: 'patch', target: 'users', content: '342' },
270
+ { type: 'patch', target: 'orders', content: '28' }
271
+ ]
272
+ });
273
+ ```
274
+
275
+ ### BwServeClient Methods
276
+
277
+ #### DOM Operations
278
+
279
+ | Method | Protocol Type | Description |
280
+ |--------|--------------|-------------|
281
+ | `client.render(target, taco)` | `replace` | Replace target contents with TACO tree |
282
+ | `client.patch(id, content, attr?)` | `patch` | Update text or attribute of element |
283
+ | `client.append(target, taco)` | `append` | Add TACO as child of target |
284
+ | `client.remove(target)` | `remove` | Remove element from DOM |
285
+ | `client.batch(ops)` | `batch` | Send multiple operations atomically |
286
+ | `client.message(target, action, data)` | `message` | Dispatch to el.bw[action] |
287
+
288
+ #### Execution Operations
289
+
290
+ | Method | Protocol Type | Description |
291
+ |--------|--------------|-------------|
292
+ | `client.register(name, body)` | `register` | Send named function to client for later call() |
293
+ | `client.call(name, ...args)` | `call` | Invoke registered or built-in function |
294
+ | `client.exec(code)` | `exec` | Execute arbitrary JS (requires client allowExec) |
295
+
296
+ #### Connection Management
297
+
298
+ | Method | Description |
299
+ |--------|-------------|
300
+ | `client.on(action, handler)` | Register handler for client actions |
301
+ | `client.close()` | Disconnect this client |
302
+
303
+ ## Screenshots
304
+
305
+ The server can capture what the client is displaying as a PNG or JPEG image. This uses html2canvas on the client side (lazy-loaded on first call, vendored at ~194 KB).
306
+
307
+ ### `client.screenshot(selector?, options?)`
308
+
309
+ Capture a screenshot of the client page or a specific element. Returns a Promise.
310
+
311
+ | Option | Type | Default | Description |
312
+ |--------|------|---------|-------------|
313
+ | `selector` | string | `'body'` | CSS selector of element to capture |
314
+ | `format` | string | `'png'` | `'png'` or `'jpeg'` |
315
+ | `quality` | number | 0.85 | JPEG quality 0–1 (ignored for PNG) |
316
+ | `maxWidth` | number | null | Resize if wider (preserves aspect ratio) |
317
+ | `maxHeight` | number | null | Resize if taller (preserves aspect ratio) |
318
+ | `scale` | number | 1 | Device pixel ratio override |
319
+ | `timeout` | number | 10000 | Reject after ms |
320
+
321
+ **Returns:** `Promise<{ data: Buffer, width: number, height: number, format: string }>`
322
+
323
+ ### Setup
324
+
325
+ Screenshot is **disabled by default**. Enable via the server option:
326
+
327
+ ```javascript
328
+ var app = bwserve.create({ port: 7902, allowScreenshot: true });
329
+ ```
330
+
331
+ ### Examples
332
+
333
+ ```javascript
334
+ // Full page screenshot
335
+ var img = await client.screenshot();
336
+ require('fs').writeFileSync('page.png', img.data);
337
+
338
+ // Element screenshot with resize
339
+ var img = await client.screenshot('#dashboard', {
340
+ format: 'jpeg',
341
+ quality: 0.8,
342
+ maxWidth: 512
343
+ });
344
+
345
+ // LLM visual feedback loop
346
+ client.render('#app', myCard);
347
+ var img = await client.screenshot('#app', { maxWidth: 800 });
348
+ var feedback = await visionModel.evaluate(img.data);
349
+ // refine TACO based on feedback...
350
+ ```
351
+
352
+ ### How it works
353
+
354
+ 1. Server calls `client.screenshot(selector, options)` — returns a Promise
355
+ 2. On first call, a capture function is registered on the client via the `register` protocol
356
+ 3. The capture function is invoked via `call` with the selector and options
357
+ 4. Client lazy-loads html2canvas (vendored, served from `/bw/lib/vendor/html2canvas.min.js`)
358
+ 5. html2canvas renders the DOM element to a `<canvas>`
359
+ 6. Client POSTs the base64 image data back to `/bw/return/screenshot/:clientId`
360
+ 7. Server converts the data URL to a Buffer and resolves the Promise
361
+
362
+ ### Security
363
+
364
+ - **Opt-in only:** `allowScreenshot: true` must be set in server options
365
+ - **DOM-level capture:** html2canvas reads the DOM — it cannot see other tabs, OS windows, or anything outside the page
366
+ - **No external requests:** html2canvas is vendored locally, not loaded from a CDN
367
+
368
+ ## Server-to-Client Execution
369
+
370
+ Beyond DOM operations, the server can invoke functions and execute code on the client. This is organized in three tiers:
371
+
372
+ ### Tier 1: `client.register(name, body)`
373
+
374
+ Send a named function to the client. The function body is a string that gets compiled once and cached. Use for reusable client-side behavior.
375
+
376
+ ```javascript
377
+ // Register an auto-scroll function on connect
378
+ client.register('autoScroll',
379
+ 'function(sel) { var el = document.querySelector(sel); if (el) el.scrollTop = el.scrollHeight; }');
380
+
381
+ // Register a formatter
382
+ client.register('formatCurrency',
383
+ 'function(id, val) { var el = document.getElementById(id); if (el) el.textContent = "$" + Number(val).toFixed(2); }');
384
+ ```
385
+
386
+ ### Tier 2: `client.call(name, ...args)`
387
+
388
+ Invoke a previously registered function or a built-in function by name. This is the workhorse for non-DOM operations — safe, lightweight, no code transfer.
389
+
390
+ ```javascript
391
+ // Call registered functions
392
+ client.call('autoScroll', '#chat');
393
+ client.call('formatCurrency', 'total', 42.5);
394
+
395
+ // Call built-in functions (always available, no registration needed)
396
+ client.call('focus', '#search-input');
397
+ client.call('scrollTo', '#bottom');
398
+ client.call('download', 'report.csv', csvContent, 'text/csv');
399
+ client.call('clipboard', 'Copied text');
400
+ client.call('redirect', '/dashboard');
401
+ client.call('log', 'Debug: user count =', users.length);
402
+ ```
403
+
404
+ **Built-in functions:**
405
+
406
+ | Name | Args | Description |
407
+ |------|------|-------------|
408
+ | `scrollTo` | `selector` | Scroll element to bottom |
409
+ | `focus` | `selector` | Focus an input element |
410
+ | `download` | `filename, content, mimeType?` | Trigger a file download |
411
+ | `clipboard` | `text` | Copy text to clipboard |
412
+ | `redirect` | `url` | Navigate to a URL |
413
+ | `log` | `...args` | console.log from the server |
414
+
415
+ ### Tier 3: `client.exec(code)`
416
+
417
+ Execute arbitrary JavaScript on the client. **Requires opt-in:** the server must be created with `allowExec: true` and/or the client connection with `{ allowExec: true }`. Without this flag, exec messages are silently rejected.
418
+
419
+ > **You probably don't need this.** If you're reaching for `exec`, consider whether `register` + `call` would work instead. `register` sends the function once; `call` invokes it by name with arguments. This covers scroll, focus, download, format, animate — essentially any reusable client-side behavior. `exec` is for truly one-off operations where registering a function would be wasteful.
420
+
421
+ ```javascript
422
+ // Server side
423
+ client.exec("document.title = 'Updated at ' + new Date().toLocaleTimeString()");
424
+ client.exec("window.scrollTo(0, 0)");
425
+ ```
426
+
427
+ **Security:** Prefer `call()` over `exec()` whenever possible. `call()` passes data as arguments (safe from injection), while `exec()` evaluates a code string. Never interpolate user input into `exec()` code strings.
428
+
429
+ ### When to Use Each Tier
430
+
431
+ | Need | Use | Why |
432
+ |------|-----|-----|
433
+ | Update the DOM | replace/patch/append/remove | Declarative, inspectable, safe |
434
+ | Simple button click | `data-bw-action` | Zero code, just an attribute |
435
+ | Scroll after append | `call("scrollTo", sel)` | Built-in, no registration |
436
+ | Trigger file download | `call("download", ...)` | Built-in, safe |
437
+ | Reusable client logic | `register` + `call` | **Default choice.** Send once, invoke many times |
438
+ | Quick one-off operation | `exec` | Last resort. No registration overhead but requires `allowExec` |
439
+ | Production security | `call` (never `exec`) | Arguments can't inject code |
440
+
441
+ > **Rule of thumb:** Start with `data-bw-action` + DOM operations. When you need client-side behavior, use `register` + `call`. Only reach for `exec` if you have a genuine one-off need and understand the security implications.
442
+
443
+ ## Client API Reference
444
+
445
+ ### Connection (bwclient.js)
446
+
447
+ SSE connection management has moved to `bwclient.js`, which is auto-generated by the bwserve shell. The shell handles opening the EventSource, reconnection, and action dispatch. You do not need to call a connection function directly.
448
+
449
+ ### `bw.apply(msg)`
450
+
451
+ Apply a single protocol message to the DOM. Called automatically by the shell connection, but also usable standalone for testing or custom transports. Handles all 9 message types.
452
+
453
+ ```javascript
454
+ // DOM operations
455
+ bw.apply({ type: 'replace', target: '#app', node: { t: 'div', c: 'Hi' } });
456
+ bw.apply({ type: 'patch', target: 'counter', content: '42' });
457
+ bw.apply({ type: 'append', target: '#list', node: { t: 'li', c: 'New' } });
458
+ bw.apply({ type: 'remove', target: '#old' });
459
+ bw.apply({ type: 'batch', ops: [msg1, msg2] });
460
+
461
+ // Execution operations
462
+ bw.apply({ type: 'register', name: 'myFn', body: 'function() { ... }' });
463
+ bw.apply({ type: 'call', name: 'myFn', args: [] });
464
+ bw.apply({ type: 'exec', code: 'alert(1)' }); // needs allowExec
465
+ ```
466
+
467
+ Returns `true` if the message was applied successfully, `false` otherwise.
468
+
469
+ ### `bw.parseJSONFlex(str)`
470
+
471
+ Parse both strict JSON and r-prefix relaxed JSON. This is a state-machine parser that converts single-quoted strings to double-quoted, strips trailing commas, and handles escape sequences (`\'`, `\\`, `\n`, `\t`). Falls back to `JSON.parse()` for strict JSON (no r-prefix).
472
+
473
+ ```javascript
474
+ // Strict JSON — passes through to JSON.parse():
475
+ bw.parseJSONFlex('{"type":"patch","target":"t","content":"42"}');
476
+
477
+ // r-prefix relaxed JSON (from ESP32 / C macros):
478
+ bw.parseJSONFlex("r{'type':'patch','target':'t','content':'42'}");
479
+
480
+ // Handles apostrophes in values:
481
+ bw.parseJSONFlex("r{'content':'Barry\\'s Room'}");
482
+ // → { content: "Barry's Room" }
483
+ ```
484
+
485
+ The shell connection calls `bw.parseJSONFlex()` on every incoming SSE message automatically. You only need to call it directly if you're building a custom transport or testing.
486
+
487
+ ## Transport
488
+
489
+ bwserve supports multiple transports:
490
+
491
+ - **SSE (default)**: Uses browser-native `EventSource`. Auto-reconnects. Best for most apps.
492
+ - **HTTP Polling**: `setInterval` + `fetch`. Works on any HTTP server including ESP32/Arduino.
493
+ - **WebSocket** (planned): Bidirectional. For high-frequency updates.
494
+
495
+ ### HTTP Endpoints
496
+
497
+ | Endpoint | Method | Purpose |
498
+ |----------|--------|---------|
499
+ | `GET /` (or registered path) | GET | Serve auto-generated page shell HTML |
500
+ | `GET /bw/events/:clientId` | GET | SSE event stream |
501
+ | `POST /bw/return/action/:clientId` | POST | User action dispatch |
502
+ | `GET /bw/lib/bitwrench.umd.js` | GET | Serve bitwrench client JS |
503
+ | `GET /bw/lib/bitwrench.css` | GET | Serve bitwrench CSS |
504
+ | `POST /bw/return/screenshot/:clientId` | POST | Screenshot POST-back from client |
505
+ | `GET /bw/lib/vendor/:filename` | GET | Serve vendored libraries (allowlisted) |
506
+
507
+ ### SSE Frame Format
508
+
509
+ Each protocol message is sent as a single SSE data frame:
510
+ ```
511
+ data: {"type":"replace","target":"#app","node":{"t":"div","c":"Hello"}}
512
+
513
+ ```
514
+
515
+ Keep-alive comments are sent every 15 seconds:
516
+ ```
517
+ :keepalive
518
+
519
+ ```
520
+
521
+ ### Embedded Device (Polling) Transport
522
+
523
+ For ESP32, Arduino, and other constrained devices. The device serves compact JSON; the browser does all rendering.
524
+
525
+ ```javascript
526
+ // Browser side: poll the device and apply messages
527
+ setInterval(function() {
528
+ fetch('/bw/ui').then(function(r) { return r.text(); }).then(function(str) {
529
+ bw.apply(bw.parseJSONFlex(str));
530
+ });
531
+ }, 1000);
532
+ ```
533
+
534
+ The device responds to `GET /bw/ui` with protocol messages using relaxed JSON (single quotes for C++ ergonomics). `bw.parseJSONFlex()` converts the relaxed format to strict JSON.
535
+
536
+ ## Relaxed JSON (r-prefix format)
537
+
538
+ For embedded C/C++ systems, composing JSON with double-quoted strings is painful — every quote needs escaping (`\"`). bwserve supports **r-prefix relaxed JSON**: single-quoted strings with an `r` prefix character.
539
+
540
+ ```
541
+ // Standard JSON in C (painful):
542
+ "{\"type\":\"patch\",\"target\":\"temp\",\"content\":\"23.5\"}"
543
+
544
+ // r-prefix relaxed JSON (natural):
545
+ "r{'type':'patch','target':'temp','content':'23.5'}"
546
+ ```
547
+
548
+ The `r` prefix tells the parser to convert single quotes to double quotes before parsing. The browser's `bw.parseJSONFlex()` handles this automatically.
549
+
550
+ ### Escaping Rule
551
+
552
+ Since single quotes delimit strings in r-prefix format, apostrophes in values must be escaped with `\'`:
553
+
554
+ ```c
555
+ // Static text with apostrophe:
556
+ BW_PATCH(msg, "room", "Barry\\'s Room");
557
+
558
+ // Dynamic user text — use BW_PATCH_SAFE (auto-escapes):
559
+ char user_text[] = "it's 23.5 C";
560
+ BW_PATCH_SAFE(msg, sizeof(msg), "status", user_text);
561
+ ```
562
+
563
+ This is still a huge win over standard JSON in C, where every quote needs `\"`.
564
+
565
+ **Direction:** r-prefix is outbound only (device to browser). The browser always sends strict JSON back via `fetch()`.
566
+
567
+ ## bwcli serve — Pipe Server
568
+
569
+ `bwcli serve` turns **any language** into a bwserve backend. It opens two ports: a **web port** (browsers connect here) and an **input port** (your app sends protocol messages here via HTTP POST). Alternatively, use `--stdin` to pipe messages from stdin.
570
+
571
+ ```
572
+ Your App (any language) bwcli serve Browser(s)
573
+ ┌───────────────────────┐ ┌───────────────┐ ┌─────────────┐
574
+ │ curl / Python / C │ │ web: :8080 │<──>│ EventSource│
575
+ │ POST to :9000 │───>│ input: :9000 │ │ bw.client..│
576
+ │ or pipe to stdin │ │ --stdin │ │ │
577
+ └───────────────────────┘ └───────────────┘ └─────────────┘
578
+ ```
579
+
580
+ ### Usage
581
+
582
+ ```bash
583
+ # Start the pipe server (dual port)
584
+ bwcli serve --port 8080 --input-port 9000
585
+
586
+ # Open browser automatically
587
+ bwcli serve --open
588
+
589
+ # Stdin mode (pipe messages from any command)
590
+ python sensor.py | bwcli serve --stdin --port 8080
591
+
592
+ # With verbose logging
593
+ bwcli serve -v
594
+ ```
595
+
596
+ ### Sending messages
597
+
598
+ ```bash
599
+ # Patch a value via curl:
600
+ curl -X POST http://localhost:9000 \
601
+ -H "Content-Type: application/json" \
602
+ -d '{"type":"patch","target":"temp","content":"23.5 C"}'
603
+
604
+ # r-prefix relaxed JSON is also accepted:
605
+ curl -X POST http://localhost:9000 -d "r{'type':'patch','target':'temp','content':'23.5'}"
606
+ ```
607
+
608
+ ### From Python
609
+
610
+ ```python
611
+ import requests, time, random
612
+
613
+ while True:
614
+ temp = 20 + random.random() * 10
615
+ requests.post("http://localhost:9000", json={
616
+ "type": "patch",
617
+ "target": "temp",
618
+ "content": f"{temp:.1f} C"
619
+ })
620
+ time.sleep(2)
621
+ ```
622
+
623
+ Both the input port and stdin mode accept strict JSON and r-prefix relaxed JSON. All messages are broadcast to every connected browser.
624
+
625
+ ## Complete Examples
626
+
627
+ ### Counter (render + patch + actions)
628
+
629
+ ```javascript
630
+ import bwserve from 'bitwrench/bwserve';
631
+
632
+ var app = bwserve.create({ port: 7902 });
633
+ var count = 0;
634
+
635
+ app.page('/', function(client) {
636
+ client.render('#app', {
637
+ t: 'div', c: [
638
+ { t: 'h2', c: 'Counter' },
639
+ { t: 'span', a: { id: 'count' }, c: '0' },
640
+ { t: 'button', a: { 'data-bw-action': 'inc' }, c: '+1' }
641
+ ]
642
+ });
643
+
644
+ client.on('inc', function() {
645
+ client.patch('count', String(++count));
646
+ });
647
+ });
648
+
649
+ app.listen();
650
+ ```
651
+
652
+ ### Todo List (append + remove)
653
+
654
+ ```javascript
655
+ app.page('/', function(client) {
656
+ var nextId = 1;
657
+
658
+ client.render('#app', {
659
+ t: 'div', c: [
660
+ { t: 'input', a: { type: 'text', id: 'inp' } },
661
+ { t: 'button', a: { 'data-bw-action': 'add' }, c: 'Add' },
662
+ { t: 'ul', a: { id: 'list' } }
663
+ ]
664
+ });
665
+
666
+ client.on('add', function(data) {
667
+ var id = 'item-' + nextId++;
668
+ client.append('#list', {
669
+ t: 'li', a: { id: id }, c: [
670
+ data.inputValue || 'item',
671
+ { t: 'button', a: { 'data-bw-action': 'del', 'data-bw-id': id }, c: 'x' }
672
+ ]
673
+ });
674
+ });
675
+
676
+ client.on('del', function(data) {
677
+ if (data.bwId) client.remove('#' + data.bwId);
678
+ });
679
+ });
680
+ ```
681
+
682
+ ### Dashboard (batch + register/call)
683
+
684
+ ```javascript
685
+ app.page('/', function(client) {
686
+ client.render('#app', {
687
+ t: 'div', c: [
688
+ { t: 'span', a: { id: 'users' }, c: '0' },
689
+ { t: 'span', a: { id: 'orders' }, c: '0' },
690
+ { t: 'span', a: { id: 'revenue' }, c: '$0' },
691
+ { t: 'div', a: { id: 'log' } }
692
+ ]
693
+ });
694
+
695
+ // Register a format function on the client
696
+ client.register('formatNum',
697
+ 'function(id, val) { var el = document.getElementById(id); if (el) el.textContent = Number(val).toLocaleString(); }');
698
+
699
+ // Update every second
700
+ setInterval(function() {
701
+ var users = Math.floor(Math.random() * 500);
702
+ var orders = Math.floor(Math.random() * 50);
703
+ client.batch([
704
+ { type: 'call', name: 'formatNum', args: ['users', users] },
705
+ { type: 'call', name: 'formatNum', args: ['orders', orders] },
706
+ { type: 'patch', target: 'revenue', content: '$' + Math.floor(Math.random() * 10000) }
707
+ ]);
708
+ }, 1000);
709
+ });
710
+ ```
711
+
712
+ ### Chat with Auto-scroll (append + register + call)
713
+
714
+ ```javascript
715
+ app.page('/', function(client) {
716
+ client.render('#app', {
717
+ t: 'div', c: [
718
+ { t: 'div', a: { id: 'chat', style: 'max-height:400px;overflow-y:auto' } },
719
+ { t: 'div', a: { style: 'display:flex;gap:8px' }, c: [
720
+ { t: 'input', a: { type: 'text', id: 'msg-input' } },
721
+ { t: 'button', a: { 'data-bw-action': 'send' }, c: 'Send' }
722
+ ]}
723
+ ]
724
+ });
725
+
726
+ // Register auto-scroll for reuse after each message
727
+ client.register('scrollChat',
728
+ 'function() { var el = document.getElementById("chat"); if (el) el.scrollTop = el.scrollHeight; }');
729
+
730
+ client.on('send', function(data) {
731
+ if (!data.inputValue) return;
732
+
733
+ // Append the message, then scroll to bottom
734
+ client.append('#chat', {
735
+ t: 'div', a: { style: 'padding:4px' }, c: data.inputValue
736
+ });
737
+ client.call('scrollChat');
738
+
739
+ // Focus back on the input
740
+ client.call('focus', '#msg-input');
741
+ });
742
+ });
743
+ ```
744
+
745
+ ### File Download (call built-in)
746
+
747
+ ```javascript
748
+ client.on('export', function(data) {
749
+ var csv = 'id,name,score\n';
750
+ records.forEach(function(r) {
751
+ csv += r.id + ',' + r.name + ',' + r.score + '\n';
752
+ });
753
+ client.call('download', 'export.csv', csv, 'text/csv');
754
+ });
755
+ ```
756
+
757
+ ## Target Use Cases
758
+
759
+ - **Streamlit-style apps**: Data dashboards, ML experiment UIs, admin panels — server computes, browser displays
760
+ - **Embedded device dashboards**: ESP32/Raspberry Pi serves a web UI using lightweight polling
761
+ - **Agent-driven UI**: An AI agent pushes UI updates and uses `client.screenshot()` for visual feedback
762
+ - **Prototyping**: Server-side Node.js logic with zero frontend build step
763
+ - **Internal tools**: Quick admin panels without frontend framework overhead
764
+
765
+ ## Attach Mode — Remote Debugging REPL
766
+
767
+ `bwcli attach` provides a built-in terminal-based debugger for any bitwrench page. It is bitwrench's answer to Playwright/Chrome DevTools — a REPL inspector that speaks the bwserve protocol.
768
+
769
+ ### Quick Start
770
+
771
+ ```bash
772
+ # Start the attach server
773
+ bwcli attach
774
+
775
+ # In the browser — add the drop-in script:
776
+ # <script src="http://localhost:7902/bw/attach.js"></script>
777
+ # Or paste in devtools console:
778
+ # var s=document.createElement('script');s.src='http://localhost:7902/bw/attach.js';document.head.appendChild(s);
779
+ ```
780
+
781
+ Once connected, you get a REPL:
782
+
783
+ ```
784
+ bw> document.title
785
+ "My Page"
786
+
787
+ bw> bw.$('.bw-card').length
788
+ 3
789
+
790
+ bw> /tree #app 2
791
+ div#app
792
+ div.main-panel
793
+ h1#title
794
+
795
+ bw> /listen button click
796
+ Listening for click on button
797
+ [event] click on button → BUTTON#save-btn "Save"
798
+
799
+ bw> /screenshot body page.png
800
+ Saved: page.png (1440x900, 245832 bytes)
801
+ ```
802
+
803
+ ### How It Works
804
+
805
+ 1. `bwcli attach` starts a bwserve instance (default port 7902)
806
+ 2. The page loads `/bw/attach.js` which injects bitwrench (if not already loaded) and connects via SSE
807
+ 3. You type JS expressions or slash commands in the terminal
808
+ 4. The server sends protocol messages; the client evaluates and POSTs results back
809
+ 5. Event listeners (via `/listen`) stream DOM events back to the terminal in real time
810
+
811
+ ### REPL Commands
812
+
813
+ | Command | Description |
814
+ |---------|-------------|
815
+ | `<expression>` | Evaluate JS in the browser (e.g., `document.title`) |
816
+ | `/tree [sel] [depth]` | DOM tree summary (default: body, depth 3) |
817
+ | `/screenshot [sel] [file]` | Capture element to PNG (requires `--allow-screenshot`) |
818
+ | `/mount <sel> <comp> [json]` | Mount a BCCL component |
819
+ | `/render <sel> <taco-json>` | Render TACO at selector |
820
+ | `/patch <id> <content>` | Update element text by ID |
821
+ | `/listen <sel> <event>` | Watch for DOM events |
822
+ | `/unlisten <sel> <event>` | Stop watching events |
823
+ | `/exec <code>` | Execute JS (fire-and-forget) |
824
+ | `/clients` | List connected clients |
825
+ | `/help`, `/quit` | Help / exit |
826
+
827
+ ### Security
828
+
829
+ Attach mode has `allowExec: true` always on — it's a debugging tool. The server binds to `localhost` by default. **Never expose to the public internet.**
830
+
831
+ For the full attach guide, see [bwcli attach documentation](bw-attach.md).
832
+
833
+ ## Related
834
+
835
+ - [Protocol Reference Page](../pages/12-bwserve-protocol.html) — Interactive protocol reference with all 9 message types
836
+ - [Sandbox](../pages/14-bwserve-sandbox.html) — Try bwserve protocol in the browser (no server needed)
837
+ - [Screenshot Example](../examples/client-server/screenshot-server.js) — Runnable screenshot demo
838
+ - [Design Document](../dev/bw-client-server.md) — Protocol design decisions and architecture
839
+ - [CLI](cli.md) — The `bwcli` command for file conversion and pipe server
840
+ - [Attach Mode](bw-attach.md) — Full remote debugging REPL documentation
841
+ - [Embedded Tutorial](tutorial-embedded.md) — ESP32 IoT dashboard with C macros and r-prefix JSON