bitwrench 2.0.31 → 2.1.0

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 (126) hide show
  1. package/README.md +210 -125
  2. package/dist/bitwrench-bccl.cjs.js +349 -188
  3. package/dist/bitwrench-bccl.cjs.min.js +2 -39
  4. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  5. package/dist/bitwrench-bccl.esm.js +349 -188
  6. package/dist/bitwrench-bccl.esm.min.js +2 -39
  7. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  8. package/dist/bitwrench-bccl.umd.js +349 -188
  9. package/dist/bitwrench-bccl.umd.min.js +2 -39
  10. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  11. package/dist/bitwrench-code-edit.cjs.js +17 -6
  12. package/dist/bitwrench-code-edit.cjs.min.js +2 -20
  13. package/dist/bitwrench-code-edit.es5.js +8 -3
  14. package/dist/bitwrench-code-edit.es5.min.js +2 -19
  15. package/dist/bitwrench-code-edit.esm.js +17 -6
  16. package/dist/bitwrench-code-edit.esm.min.js +2 -19
  17. package/dist/bitwrench-code-edit.umd.js +17 -6
  18. package/dist/bitwrench-code-edit.umd.min.js +2 -19
  19. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  20. package/dist/bitwrench-debug.js +1 -1
  21. package/dist/bitwrench-debug.min.js +1 -1
  22. package/dist/bitwrench-lean.cjs.js +2492 -1628
  23. package/dist/bitwrench-lean.cjs.min.js +2 -80
  24. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  25. package/dist/bitwrench-lean.es5.js +2740 -1838
  26. package/dist/bitwrench-lean.es5.min.js +2 -49
  27. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  28. package/dist/bitwrench-lean.esm.js +2492 -1628
  29. package/dist/bitwrench-lean.esm.min.js +2 -80
  30. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  31. package/dist/bitwrench-lean.umd.js +2492 -1628
  32. package/dist/bitwrench-lean.umd.min.js +2 -80
  33. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  34. package/dist/bitwrench-util-color.cjs.js +251 -0
  35. package/dist/bitwrench-util-color.cjs.min.js +3 -0
  36. package/dist/bitwrench-util-color.es5.js +256 -0
  37. package/dist/bitwrench-util-color.es5.min.js +3 -0
  38. package/dist/bitwrench-util-color.esm.js +241 -0
  39. package/dist/bitwrench-util-color.esm.min.js +3 -0
  40. package/dist/bitwrench-util-color.umd.js +257 -0
  41. package/dist/bitwrench-util-color.umd.min.js +3 -0
  42. package/dist/bitwrench-util-css.cjs.js +2 -1
  43. package/dist/bitwrench-util-css.cjs.min.js +2 -21
  44. package/dist/bitwrench-util-css.es5.js +2 -1
  45. package/dist/bitwrench-util-css.es5.min.js +2 -20
  46. package/dist/bitwrench-util-css.esm.js +2 -1
  47. package/dist/bitwrench-util-css.esm.min.js +1 -19
  48. package/dist/bitwrench-util-css.umd.js +2 -1
  49. package/dist/bitwrench-util-css.umd.min.js +2 -20
  50. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  51. package/dist/bitwrench.cjs.js +2826 -1801
  52. package/dist/bitwrench.cjs.min.js +2 -99
  53. package/dist/bitwrench.cjs.min.js.gz +0 -0
  54. package/dist/bitwrench.css +403 -479
  55. package/dist/bitwrench.d.ts +70 -73
  56. package/dist/bitwrench.es5.js +3106 -2020
  57. package/dist/bitwrench.es5.min.js +2 -67
  58. package/dist/bitwrench.es5.min.js.gz +0 -0
  59. package/dist/bitwrench.esm.js +2826 -1801
  60. package/dist/bitwrench.esm.min.js +2 -99
  61. package/dist/bitwrench.esm.min.js.gz +0 -0
  62. package/dist/bitwrench.min.css +1 -1
  63. package/dist/bitwrench.umd.js +2826 -1801
  64. package/dist/bitwrench.umd.min.js +2 -99
  65. package/dist/bitwrench.umd.min.js.gz +0 -0
  66. package/dist/builds.json +222 -134
  67. package/dist/bwserve.cjs.js +308 -298
  68. package/dist/bwserve.d.ts +157 -0
  69. package/dist/bwserve.esm.js +309 -299
  70. package/dist/sri.json +54 -46
  71. package/docs/README.md +6 -3
  72. package/docs/app-patterns.md +7 -6
  73. package/docs/bitwrench-for-wasm.md +53 -54
  74. package/docs/bitwrench-mcp.md +2 -2
  75. package/docs/bitwrench-northstar-principles.md +406 -0
  76. package/docs/bitwrench-taco-schema-discussion.md +2 -2
  77. package/docs/bitwrench_api.md +191 -106
  78. package/docs/bitwrench_typescript_usage.md +5 -5
  79. package/docs/bw-attach.md +29 -75
  80. package/docs/bwserve.md +200 -168
  81. package/docs/cli.md +36 -12
  82. package/docs/component-cheatsheet.md +2 -2
  83. package/docs/component-library.md +4 -4
  84. package/docs/component-lifecycle.md +234 -0
  85. package/docs/drift-lint.md +268 -0
  86. package/docs/framework-translation-table.md +4 -4
  87. package/docs/llm-bitwrench-guide.md +60 -50
  88. package/docs/routing.md +11 -13
  89. package/docs/state-management.md +110 -109
  90. package/docs/taco-format.md +13 -14
  91. package/docs/theming.md +13 -3
  92. package/docs/thinking-in-bitwrench.md +858 -983
  93. package/docs/tutorial-bwserve.md +37 -36
  94. package/docs/tutorial-embedded.md +10 -21
  95. package/docs/tutorial-website.md +2 -2
  96. package/package.json +38 -7
  97. package/readme.html +262 -160
  98. package/src/bitwrench-bccl-entry.js +2 -2
  99. package/src/bitwrench-bccl.js +346 -185
  100. package/src/bitwrench-code-edit.js +16 -5
  101. package/src/bitwrench-color-utils.js +117 -181
  102. package/src/bitwrench-file-ops.js +2 -2
  103. package/src/bitwrench-lean.js +4 -3
  104. package/src/bitwrench-router.js +5 -2
  105. package/src/bitwrench-styles.js +420 -504
  106. package/src/bitwrench-util-color.js +240 -0
  107. package/src/bitwrench-util-css.js +1 -0
  108. package/src/bitwrench-utils.js +4 -0
  109. package/src/bitwrench.d.ts +70 -73
  110. package/src/bitwrench.h +5 -0
  111. package/src/bitwrench.js +1939 -933
  112. package/src/bwserve/attach.js +0 -1
  113. package/src/bwserve/bwclient.js +172 -32
  114. package/src/bwserve/bwshell.js +0 -4
  115. package/src/bwserve/client.js +59 -220
  116. package/src/bwserve/index.js +78 -42
  117. package/src/bwserve.d.ts +157 -0
  118. package/src/bwserve.h +5 -0
  119. package/src/cli/attach.js +12 -75
  120. package/src/cli/convert.js +2 -2
  121. package/src/cli/serve.js +37 -35
  122. package/src/generate-css.js +1 -1
  123. package/src/mcp/knowledge.js +4 -4
  124. package/src/mcp/live.js +21 -13
  125. package/src/mcp/tools.js +0 -1
  126. package/src/version.js +3 -7
package/docs/bwserve.md CHANGED
@@ -4,13 +4,13 @@
4
4
 
5
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
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.
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 (~45KB), so it renders TACO natively with no build step.
8
8
 
9
9
  **Key characteristics:**
10
10
  - Zero runtime dependencies — only Node.js stdlib (`http`, `fs`, `path`)
11
11
  - Auto-generates the client page (loads bitwrench, opens SSE, wires actions)
12
12
  - Same protocol works for Node.js servers and ESP32/Arduino embedded devices
13
- - 9 protocol message types covering DOM operations and remote execution
13
+ - 13 protocol message types (hello, mount, patch, append, replace, remove, refresh, update, message, batch, call, listen, unlisten)
14
14
 
15
15
  ## Architecture
16
16
 
@@ -22,18 +22,16 @@ This is the same pattern as Streamlit (Python), Phoenix LiveView (Elixir), and h
22
22
  │ bwclient.js ├──────────────> app.page('/', fn) │
23
23
  │ (opens SSE) │ │ │
24
24
  │ │<──SSE──────── │ DOM operations: │
25
- │ bw.apply() │ {type,node} │ client.render()
25
+ │ bw.apply() │ {type,taco} │ client.mount()
26
26
  │ -> bw.DOM() │ │ client.patch() │
27
27
  │ -> bw.patch() │ │ client.append() │
28
28
  │ │ │ client.remove() │
29
29
  │ │ │ client.batch() │
30
30
  │ │ │ │
31
- │ │ │ Execution:
32
- │ │ │ client.register() │
31
+ │ │ │ Other:
33
32
  │ │ │ client.call() │
34
- │ │ │ client.exec()
35
- │ │
36
- │ data-bw-action │ │ │
33
+ │ │ │ client.message()
34
+ bw_act_* class │ │ client.listen()
37
35
  │ conn.sendAction │──POST───────> │ client.on(action,fn) │
38
36
  │ │ {action,data}│ │
39
37
  └──────────────────┘ └──────────────────────┘
@@ -44,7 +42,7 @@ This is the same pattern as Streamlit (Python), Phoenix LiveView (Elixir), and h
44
42
  2. Shell loads bitwrench from `/bw/lib/bitwrench.umd.js` and opens SSE
45
43
  3. SSE triggers page handler → server sends TACO rendering commands
46
44
  4. Browser applies each message via `bw.apply()` → DOM updates
47
- 5. User clicks → `data-bw-action` triggers POST → server handler runs
45
+ 5. User clicks → `bw_act_*` class triggers POST → server handler runs
48
46
  6. Server sends more messages → browser updates. Loop continues.
49
47
 
50
48
  ## Quick Start
@@ -57,28 +55,28 @@ var app = bwserve.create({ port: 7902 });
57
55
  var count = 0;
58
56
 
59
57
  app.page('/', function(client) {
60
- // 1. Render the initial UI as a TACO tree
61
- client.render('#app', {
58
+ // 1. Mount the initial UI as a TACO tree
59
+ client.mount('#app', {
62
60
  t: 'div', a: { style: 'padding:24px' }, c: [
63
61
  { t: 'h1', c: 'Counter' },
64
62
  { t: 'p', c: [
65
63
  'Count: ',
66
64
  { t: 'span', a: { id: 'count', style: 'font-weight:bold' }, c: '0' }
67
65
  ]},
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' }
66
+ { t: 'button', a: { class: 'bw_bccl_btn bw_primary bw_act_increment' }, c: '+1' },
67
+ { t: 'button', a: { class: 'bw_bccl_btn bw_bccl_btn_outline bw_secondary bw_act_reset' }, c: 'Reset' }
70
68
  ]
71
69
  });
72
70
 
73
71
  // 2. Handle user actions
74
72
  client.on('increment', function() {
75
73
  count++;
76
- client.patch('count', String(count));
74
+ client.patch('count', { text: String(count) });
77
75
  });
78
76
 
79
77
  client.on('reset', function() {
80
78
  count = 0;
81
- client.patch('count', '0');
79
+ client.patch('count', { text: '0' });
82
80
  });
83
81
  });
84
82
 
@@ -91,7 +89,7 @@ Save as `server.js`, run `node server.js`, open `http://localhost:7902`.
91
89
 
92
90
  ## Protocol Messages
93
91
 
94
- bwserve uses 9 message types, organized in two categories:
92
+ bwserve uses 13 message types. All messages are stamped with `v: 1` (wire protocol version) and use `ref` for target references and `taco` for TACO payloads.
95
93
 
96
94
  ### DOM Operations (5 types)
97
95
 
@@ -99,64 +97,64 @@ These modify the browser's DOM tree:
99
97
 
100
98
  | Type | Purpose | Server Method | Client Action |
101
99
  |------|---------|---------------|---------------|
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()` |
100
+ | `mount` | Mount TACO at selector | `client.mount(ref, taco)` | `bw.DOM(ref, taco)` |
101
+ | `patch` | Update text/attributes | `client.patch(ref, fields)` | `bw.patch(ref, fields)` |
102
+ | `append` | Add child element | `client.append(ref, taco)` | `ref.appendChild(bw.create(taco))` |
103
+ | `remove` | Remove element | `client.remove(ref)` | `bw.unmount(el); el.remove()` |
106
104
  | `batch` | Multiple operations | `client.batch(ops)` | Execute each op in sequence |
107
105
 
108
- ### Execution Operations (3 types)
109
-
110
- These invoke functions or execute code on the client:
106
+ ### Communication Operations (5 types)
111
107
 
112
108
  | Type | Purpose | Server Method | Client Action |
113
109
  |------|---------|---------------|---------------|
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
110
+ | `call` | Invoke built-in function | `client.call(name, ...args)` | Call built-in function (scrollTo, focus, download, etc.) |
111
+ | `message` | Component dispatch | `client.message(ref, action, data)` | `bw.message(ref, action, data)` |
112
+ | `listen` | Subscribe to topic | `client.listen(topic, handler)` | Forward matching events to server |
113
+ | `unlisten` | Unsubscribe from topic | (automatic on disconnect) | Stop forwarding events |
114
+ | `hello` | Handshake | (automatic on connect) | Establish connection |
119
115
 
120
- | Type | Purpose | Server Method | Client Action |
121
- |------|---------|---------------|---------------|
122
- | `message` | Component dispatch | `client.message(target, action, data)` | `bw.message(target, action, data)` |
116
+ > **Note:** `exec` and `register` were removed in v2.1 for security reasons (arbitrary server-defined code should never cross the wire). `query` remains as a safe eval of caller-supplied expressions. Use `bw_act_*` class patterns for user actions, and `client.call()` for built-in client-side operations like scrollTo, focus, download, clipboard, and redirect.
123
117
 
124
118
  ### Message Schemas
125
119
 
120
+ All messages include `v: 1` (wire protocol version).
121
+
126
122
  ```json
127
123
  // --- DOM Operations ---
128
124
 
129
- // replace — full subtree replacement
130
- { "type": "replace", "target": "#app", "node": {"t":"div","c":"Hello"} }
125
+ // mount — full subtree mount
126
+ { "v": 1, "type": "mount", "ref": "#app", "taco": {"t":"div","c":"Hello"} }
131
127
 
132
- // patch — lightweight text/attribute update
133
- { "type": "patch", "target": "counter", "content": "42" }
134
- { "type": "patch", "target": "status", "content": "active", "attr": {"class": "done"} }
128
+ // patch — lightweight text/attribute update (discriminated fields)
129
+ { "v": 1, "type": "patch", "ref": "counter", "text": "42" }
130
+ { "v": 1, "type": "patch", "ref": "status", "text": "active", "attrs": {"class": "done"} }
135
131
 
136
132
  // append — add a child
137
- { "type": "append", "target": "#list", "node": {"t":"li","c":"New item"} }
133
+ { "v": 1, "type": "append", "ref": "#list", "taco": {"t":"li","c":"New item"} }
138
134
 
139
135
  // remove — delete from DOM
140
- { "type": "remove", "target": "#old-item" }
136
+ { "v": 1, "type": "remove", "ref": "#old-item" }
141
137
 
142
138
  // batch — multi-update
143
- { "type": "batch", "ops": [
144
- { "type": "patch", "target": "a", "content": "1" },
145
- { "type": "patch", "target": "b", "content": "2" }
139
+ { "v": 1, "type": "batch", "ops": [
140
+ { "type": "patch", "ref": "a", "text": "1" },
141
+ { "type": "patch", "ref": "b", "text": "2" }
146
142
  ]}
147
143
 
148
- // --- Execution Operations ---
144
+ // --- Communication Operations ---
145
+
146
+ // call — invoke a built-in function
147
+ { "v": 1, "type": "call", "name": "focus", "args": ["#search-input"] }
148
+ { "v": 1, "type": "call", "name": "download", "args": ["report.csv", "id,name\n1,Alice", "text/csv"] }
149
149
 
150
- // registersend 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; }" }
150
+ // messagecomponent dispatch
151
+ { "v": 1, "type": "message", "ref": "#my-component", "action": "update", "data": {"value": 42} }
152
152
 
153
- // callinvoke 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"] }
153
+ // listensubscribe to topic
154
+ { "v": 1, "type": "listen", "topic": "bw:lifecycle" }
157
155
 
158
- // execexecute arbitrary JS (requires allowExec on client)
159
- { "type": "exec", "code": "document.title = 'Updated'" }
156
+ // hellohandshake (sent automatically on connect)
157
+ { "v": 1, "type": "hello" }
160
158
  ```
161
159
 
162
160
  ### Target Resolution
@@ -166,16 +164,16 @@ All DOM operation targets are resolved using:
166
164
  | Pattern | Resolution | Example |
167
165
  |---------|-----------|---------|
168
166
  | `#selector` | CSS selector via `querySelector` | `#app`, `#counter` |
169
- | `.selector` | CSS class selector | `.bw-card` |
170
- | `bare-string` | `getElementById`, then `bw._el()` fallback | `counter` |
167
+ | `.selector` | CSS class selector | `.bw_bccl_card` |
168
+ | `bare-string` | `getElementById`, then `bw.el()` fallback | `counter` |
171
169
 
172
170
  **Best practice:** Use simple `id` attributes for patchable elements:
173
171
  ```javascript
174
- // Server sends render with id:
175
- client.render('#app', { t: 'span', a: { id: 'count' }, c: '0' });
172
+ // Server mounts with id:
173
+ client.mount('#app', { t: 'span', a: { id: 'count' }, c: '0' });
176
174
 
177
175
  // Later, server patches by id:
178
- client.patch('count', '42');
176
+ client.patch('count', { text: '42' });
179
177
  ```
180
178
 
181
179
  ### Actions (Client → Server)
@@ -189,18 +187,18 @@ Content-Type: application/json
189
187
  { "action": "increment", "data": { "inputValue": "hello" } }
190
188
  ```
191
189
 
192
- **Wiring actions** — add `data-bw-action` to any element:
190
+ **Wiring actions** — use `bw_act_*` CSS classes on elements:
193
191
 
194
192
  ```javascript
195
- // Server sends this TACO:
196
- { t: 'button', a: { 'data-bw-action': 'save', class: 'bw-btn' }, c: 'Save' }
193
+ // Server sends this TACO with bw_act_* class:
194
+ { t: 'button', a: { class: 'bw_bccl_btn bw_act_save' }, c: 'Save' }
197
195
 
198
196
  // When clicked, client auto-POSTs:
199
197
  { action: 'save', data: {} }
200
198
 
201
199
  // Server handles it:
202
200
  client.on('save', function(data) {
203
- client.patch('status', 'Saved!');
201
+ client.patch('status', { text: 'Saved!' });
204
202
  });
205
203
  ```
206
204
 
@@ -219,19 +217,16 @@ Create a bwserve application.
219
217
  | `static` | string | null | Static file directory |
220
218
  | `theme` | string/object | null | Theme preset name or config |
221
219
  | `injectBitwrench` | boolean | true | Auto-inject bitwrench UMD + CSS |
222
- | `allowExec` | boolean | false | Enable `exec` messages on client (see warning below) |
223
220
  | `allowScreenshot` | boolean | false | Enable `client.screenshot()` capability |
224
221
  | `keepAliveInterval` | number | 15000 | SSE keep-alive interval in ms |
225
222
 
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
223
  ### `app.page(path, handler)`
229
224
 
230
225
  Register a page handler. The `handler` function is called with a `BwServeClient` when a browser connects via SSE.
231
226
 
232
227
  ```javascript
233
228
  app.page('/', function(client) {
234
- client.render('#app', { t: 'div', c: 'Hello' });
229
+ client.mount('#app', { t: 'div', c: 'Hello' });
235
230
  });
236
231
 
237
232
  app.page('/dashboard', function(client) {
@@ -258,16 +253,16 @@ Send a protocol message to all connected clients. Useful for dashboards, notific
258
253
 
259
254
  ```javascript
260
255
  // Broadcast a patch to all browsers:
261
- app.broadcast({ type: 'patch', target: 'status', content: 'System OK' });
256
+ app.broadcast({ type: 'patch', ref: 'status', text: 'System OK' });
262
257
 
263
258
  // Target a specific client by setting clientId:
264
- app.broadcast({ type: 'patch', target: 'msg', content: 'Hello', clientId: 'c1' });
259
+ app.broadcast({ type: 'patch', ref: 'msg', text: 'Hello', clientId: 'c1' });
265
260
 
266
261
  // Broadcast a batch update:
267
262
  app.broadcast({
268
263
  type: 'batch', ops: [
269
- { type: 'patch', target: 'users', content: '342' },
270
- { type: 'patch', target: 'orders', content: '28' }
264
+ { type: 'patch', ref: 'users', text: '342' },
265
+ { type: 'patch', ref: 'orders', text: '28' }
271
266
  ]
272
267
  });
273
268
  ```
@@ -278,20 +273,19 @@ app.broadcast({
278
273
 
279
274
  | Method | Protocol Type | Description |
280
275
  |--------|--------------|-------------|
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 |
276
+ | `client.mount(ref, taco)` | `mount` | Mount TACO tree at ref |
277
+ | `client.patch(ref, fields)` | `patch` | Update element with discriminated fields (e.g. `{text:'...'}`, `{attrs:{...}}`) |
278
+ | `client.append(ref, taco)` | `append` | Add TACO as child of ref |
279
+ | `client.remove(ref)` | `remove` | Remove element from DOM |
285
280
  | `client.batch(ops)` | `batch` | Send multiple operations atomically |
286
- | `client.message(target, action, data)` | `message` | Dispatch to el.bw[action] |
287
281
 
288
- #### Execution Operations
282
+ #### Communication
289
283
 
290
284
  | Method | Protocol Type | Description |
291
285
  |--------|--------------|-------------|
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) |
286
+ | `client.call(name, ...args)` | `call` | Invoke built-in function |
287
+ | `client.message(ref, action, data)` | `message` | Dispatch to el.bw[action] |
288
+ | `client.listen(topic, handler)` | `listen` | Subscribe to client-side topic |
295
289
 
296
290
  #### Connection Management
297
291
 
@@ -343,7 +337,7 @@ var img = await client.screenshot('#dashboard', {
343
337
  });
344
338
 
345
339
  // LLM visual feedback loop
346
- client.render('#app', myCard);
340
+ client.mount('#app', myCard);
347
341
  var img = await client.screenshot('#app', { maxWidth: 800 });
348
342
  var feedback = await visionModel.evaluate(img.data);
349
343
  // refine TACO based on feedback...
@@ -352,7 +346,7 @@ var feedback = await visionModel.evaluate(img.data);
352
346
  ### How it works
353
347
 
354
348
  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
349
+ 2. On first call, a capture function is set up on the client
356
350
  3. The capture function is invoked via `call` with the selector and options
357
351
  4. Client lazy-loads html2canvas (vendored, served from `/bw/lib/vendor/html2canvas.min.js`)
358
352
  5. html2canvas renders the DOM element to a `<canvas>`
@@ -365,34 +359,16 @@ var feedback = await visionModel.evaluate(img.data);
365
359
  - **DOM-level capture:** html2canvas reads the DOM — it cannot see other tabs, OS windows, or anything outside the page
366
360
  - **No external requests:** html2canvas is vendored locally, not loaded from a CDN
367
361
 
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)`
362
+ ## Server-to-Client Operations
373
363
 
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.
364
+ Beyond DOM operations, the server can invoke functions on the client.
375
365
 
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
- ```
366
+ ### `client.call(name, ...args)`
385
367
 
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.
368
+ Invoke a built-in function by name. This is the workhorse for non-DOM operations — safe, lightweight, no code transfer.
389
369
 
390
370
  ```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)
371
+ // Call built-in functions (always available)
396
372
  client.call('focus', '#search-input');
397
373
  client.call('scrollTo', '#bottom');
398
374
  client.call('download', 'report.csv', csvContent, 'text/csv');
@@ -412,33 +388,37 @@ client.call('log', 'Debug: user count =', users.length);
412
388
  | `redirect` | `url` | Navigate to a URL |
413
389
  | `log` | `...args` | console.log from the server |
414
390
 
415
- ### Tier 3: `client.exec(code)`
391
+ ### `client.message(ref, action, data)`
416
392
 
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.
393
+ Dispatch to a BCCL component's handle method. The client calls `bw.message(ref, action, data)` which invokes `el.bw[action](data)` on the target component.
420
394
 
421
395
  ```javascript
422
- // Server side
423
- client.exec("document.title = 'Updated at ' + new Date().toLocaleTimeString()");
424
- client.exec("window.scrollTo(0, 0)");
396
+ client.message('#my-slider', 'setValue', 75);
397
+ client.message('#my-modal', 'open', { title: 'Confirm' });
425
398
  ```
426
399
 
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.
400
+ ### `client.listen(topic, handler)`
401
+
402
+ Subscribe to a client-side topic. The client forwards matching events back to the server via the return route.
403
+
404
+ ```javascript
405
+ client.listen('bw:lifecycle', function(data) {
406
+ console.log('Lifecycle event:', data);
407
+ });
408
+ ```
428
409
 
429
410
  ### When to Use Each Tier
430
411
 
431
412
  | Need | Use | Why |
432
413
  |------|-----|-----|
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 |
414
+ | Update the DOM | mount/patch/append/remove | Declarative, inspectable, safe |
415
+ | Simple button click | `bw_act_*` class | Zero code, just a class |
416
+ | Scroll after append | `call("scrollTo", sel)` | Built-in, no code transfer |
436
417
  | 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 |
418
+ | Component interaction | `message(ref, action, data)` | Dispatches to el.bw[action] |
419
+ | Event subscription | `listen(topic, handler)` | Server observes client events |
440
420
 
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.
421
+ > **Rule of thumb:** Start with `bw_act_*` classes + DOM operations. When you need client-side behavior, use `call` with built-in functions. For component communication, use `message`.
442
422
 
443
423
  ## Client API Reference
444
424
 
@@ -448,20 +428,19 @@ SSE connection management has moved to `bwclient.js`, which is auto-generated by
448
428
 
449
429
  ### `bw.apply(msg)`
450
430
 
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.
431
+ 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 13 message types.
452
432
 
453
433
  ```javascript
454
434
  // 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] });
435
+ bw.apply({ v: 1, type: 'mount', ref: '#app', taco: { t: 'div', c: 'Hi' } });
436
+ bw.apply({ v: 1, type: 'patch', ref: 'counter', text: '42' });
437
+ bw.apply({ v: 1, type: 'append', ref: '#list', taco: { t: 'li', c: 'New' } });
438
+ bw.apply({ v: 1, type: 'remove', ref: '#old' });
439
+ bw.apply({ v: 1, type: 'batch', ops: [msg1, msg2] });
460
440
 
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
441
+ // Communication operations
442
+ bw.apply({ v: 1, type: 'call', name: 'scrollTo', args: ['#chat'] });
443
+ bw.apply({ v: 1, type: 'message', ref: '#comp', action: 'update', data: {} });
465
444
  ```
466
445
 
467
446
  Returns `true` if the message was applied successfully, `false` otherwise.
@@ -472,14 +451,14 @@ Parse both strict JSON and r-prefix relaxed JSON. This is a state-machine parser
472
451
 
473
452
  ```javascript
474
453
  // Strict JSON — passes through to JSON.parse():
475
- bw.parseJSONFlex('{"type":"patch","target":"t","content":"42"}');
454
+ bw.parseJSONFlex('{"type":"patch","ref":"t","text":"42"}');
476
455
 
477
456
  // r-prefix relaxed JSON (from ESP32 / C macros):
478
- bw.parseJSONFlex("r{'type':'patch','target':'t','content':'42'}");
457
+ bw.parseJSONFlex("r{'type':'patch','ref':'t','text':'42'}");
479
458
 
480
459
  // Handles apostrophes in values:
481
- bw.parseJSONFlex("r{'content':'Barry\\'s Room'}");
482
- // → { content: "Barry's Room" }
460
+ bw.parseJSONFlex("r{'text':'Barry\\'s Room'}");
461
+ // → { text: "Barry's Room" }
483
462
  ```
484
463
 
485
464
  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.
@@ -508,7 +487,7 @@ bwserve supports multiple transports:
508
487
 
509
488
  Each protocol message is sent as a single SSE data frame:
510
489
  ```
511
- data: {"type":"replace","target":"#app","node":{"t":"div","c":"Hello"}}
490
+ data: {"v":1,"type":"mount","ref":"#app","taco":{"t":"div","c":"Hello"}}
512
491
 
513
492
  ```
514
493
 
@@ -539,10 +518,10 @@ For embedded C/C++ systems, composing JSON with double-quoted strings is painful
539
518
 
540
519
  ```
541
520
  // Standard JSON in C (painful):
542
- "{\"type\":\"patch\",\"target\":\"temp\",\"content\":\"23.5\"}"
521
+ "{\"type\":\"patch\",\"ref\":\"temp\",\"text\":\"23.5\"}"
543
522
 
544
523
  // r-prefix relaxed JSON (natural):
545
- "r{'type':'patch','target':'temp','content':'23.5'}"
524
+ "r{'type':'patch','ref':'temp','text':'23.5'}"
546
525
  ```
547
526
 
548
527
  The `r` prefix tells the parser to convert single quotes to double quotes before parsing. The browser's `bw.parseJSONFlex()` handles this automatically.
@@ -599,10 +578,10 @@ bwcli serve -v
599
578
  # Patch a value via curl:
600
579
  curl -X POST http://localhost:9000 \
601
580
  -H "Content-Type: application/json" \
602
- -d '{"type":"patch","target":"temp","content":"23.5 C"}'
581
+ -d '{"type":"patch","ref":"temp","text":"23.5 C"}'
603
582
 
604
583
  # r-prefix relaxed JSON is also accepted:
605
- curl -X POST http://localhost:9000 -d "r{'type':'patch','target':'temp','content':'23.5'}"
584
+ curl -X POST http://localhost:9000 -d "r{'type':'patch','ref':'temp','text':'23.5'}"
606
585
  ```
607
586
 
608
587
  ### From Python
@@ -614,17 +593,77 @@ while True:
614
593
  temp = 20 + random.random() * 10
615
594
  requests.post("http://localhost:9000", json={
616
595
  "type": "patch",
617
- "target": "temp",
618
- "content": f"{temp:.1f} C"
596
+ "ref": "temp",
597
+ "text": f"{temp:.1f} C"
619
598
  })
620
599
  time.sleep(2)
621
600
  ```
622
601
 
623
602
  Both the input port and stdin mode accept strict JSON and r-prefix relaxed JSON. All messages are broadcast to every connected browser.
624
603
 
604
+ ### Interactive Commands
605
+
606
+ Messages with a `"command"` field are interactive requests -- they return a JSON response instead of broadcasting. Use these to inspect or query connected clients.
607
+
608
+ ```bash
609
+ # List connected clients
610
+ curl -X POST http://localhost:9000 -d '{"command":"clients"}'
611
+ # => {"ok":true,"clients":["att_x7k2m"]}
612
+
613
+ # Evaluate a JS expression in the browser
614
+ curl -X POST http://localhost:9000 -d '{"command":"query","code":"document.title"}'
615
+ # => {"ok":true,"result":"My Page","clientId":"att_x7k2m"}
616
+
617
+ # Get a DOM tree snapshot
618
+ curl -X POST http://localhost:9000 -d '{"command":"tree","selector":"#app","depth":2}'
619
+ # => {"ok":true,"result":{"tag":"div","id":"app","children":[...]},"clientId":"att_x7k2m"}
620
+
621
+ # Take a screenshot (requires --allow-screenshot)
622
+ curl -X POST http://localhost:9000 -d '{"command":"screenshot","selector":"body"}'
623
+ # => {"ok":true,"result":{"data":"<base64>","width":1024,"height":768,"format":"png"},"clientId":"att_x7k2m"}
624
+
625
+ # Mount a component
626
+ curl -X POST http://localhost:9000 -d '{"command":"mount","ref":"#app","taco":{"t":"h1","c":"Hello"}}'
627
+ # => {"ok":true,"clientId":"att_x7k2m"}
628
+
629
+ # Target a specific client
630
+ curl -X POST http://localhost:9000 -d '{"command":"query","code":"location.href","clientId":"att_x7k2m"}'
631
+ ```
632
+
633
+ | Command | Required Fields | Returns | Notes |
634
+ |---------|----------------|---------|-------|
635
+ | `clients` | -- | `{ok, clients: [...]}` | List all connected client IDs |
636
+ | `query` | `code` | `{ok, result, clientId}` | Eval expression in browser; expressions auto-wrapped in `return()`, statements work too |
637
+ | `tree` | -- | `{ok, result, clientId}` | DOM tree snapshot; optional `selector` (default: body), `depth` (default: 3) |
638
+ | `screenshot` | -- | `{ok, result, clientId}` | Requires `--allow-screenshot` flag; optional `selector` |
639
+ | `mount` | `ref`, `taco` | `{ok, clientId}` | Mount TACO at selector |
640
+ | `patch` | `ref` | `{ok, clientId}` | Update text/attrs; extra fields forwarded |
641
+ | `listen` | `topic` | `{ok, clientId}` | Subscribe to DOM events |
642
+ | `unlisten` | `topic` | `{ok, clientId}` | Unsubscribe from events |
643
+
644
+ When no `clientId` is specified, the command routes to the most recently connected client.
645
+
646
+ ### CLI Options
647
+
648
+ ```
649
+ bwcli serve [dir] [options]
650
+
651
+ -p, --port <number> Browser-facing web port (default: 8080)
652
+ -l, --listen <number> Input port for protocol messages (default: 9000)
653
+ -b, --bind <address> Host/address to bind to (default: 0.0.0.0)
654
+ --stdin Read protocol messages from stdin
655
+ -t, --theme <name> Theme preset or hex colors ("#pri,#sec")
656
+ --title <string> Page title (default: "bwcli serve")
657
+ --no-dir-list Disable directory listings
658
+ --allow-screenshot Enable client.screenshot() capability
659
+ --open Open browser on start
660
+ -v, --verbose Verbose output
661
+ -h, --help Print help
662
+ ```
663
+
625
664
  ## Complete Examples
626
665
 
627
- ### Counter (render + patch + actions)
666
+ ### Counter (mount + patch + actions)
628
667
 
629
668
  ```javascript
630
669
  import bwserve from 'bitwrench/bwserve';
@@ -633,16 +672,16 @@ var app = bwserve.create({ port: 7902 });
633
672
  var count = 0;
634
673
 
635
674
  app.page('/', function(client) {
636
- client.render('#app', {
675
+ client.mount('#app', {
637
676
  t: 'div', c: [
638
677
  { t: 'h2', c: 'Counter' },
639
678
  { t: 'span', a: { id: 'count' }, c: '0' },
640
- { t: 'button', a: { 'data-bw-action': 'inc' }, c: '+1' }
679
+ { t: 'button', a: { class: 'bw_act_inc' }, c: '+1' }
641
680
  ]
642
681
  });
643
682
 
644
683
  client.on('inc', function() {
645
- client.patch('count', String(++count));
684
+ client.patch('count', { text: String(++count) });
646
685
  });
647
686
  });
648
687
 
@@ -655,10 +694,10 @@ app.listen();
655
694
  app.page('/', function(client) {
656
695
  var nextId = 1;
657
696
 
658
- client.render('#app', {
697
+ client.mount('#app', {
659
698
  t: 'div', c: [
660
699
  { t: 'input', a: { type: 'text', id: 'inp' } },
661
- { t: 'button', a: { 'data-bw-action': 'add' }, c: 'Add' },
700
+ { t: 'button', a: { class: 'bw_act_add' }, c: 'Add' },
662
701
  { t: 'ul', a: { id: 'list' } }
663
702
  ]
664
703
  });
@@ -668,22 +707,25 @@ app.page('/', function(client) {
668
707
  client.append('#list', {
669
708
  t: 'li', a: { id: id }, c: [
670
709
  data.inputValue || 'item',
671
- { t: 'button', a: { 'data-bw-action': 'del', 'data-bw-id': id }, c: 'x' }
710
+ { t: 'button', a: { id: id + '-del', class: 'bw_act_del' }, c: 'x' }
672
711
  ]
673
712
  });
674
713
  });
675
714
 
676
715
  client.on('del', function(data) {
677
- if (data.bwId) client.remove('#' + data.bwId);
716
+ if (data.id) {
717
+ var itemId = data.id.replace('-del', '');
718
+ client.remove('#' + itemId);
719
+ }
678
720
  });
679
721
  });
680
722
  ```
681
723
 
682
- ### Dashboard (batch + register/call)
724
+ ### Dashboard (batch + call)
683
725
 
684
726
  ```javascript
685
727
  app.page('/', function(client) {
686
- client.render('#app', {
728
+ client.mount('#app', {
687
729
  t: 'div', c: [
688
730
  { t: 'span', a: { id: 'users' }, c: '0' },
689
731
  { t: 'span', a: { id: 'orders' }, c: '0' },
@@ -692,41 +734,33 @@ app.page('/', function(client) {
692
734
  ]
693
735
  });
694
736
 
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
737
  // Update every second
700
738
  setInterval(function() {
701
739
  var users = Math.floor(Math.random() * 500);
702
740
  var orders = Math.floor(Math.random() * 50);
703
741
  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) }
742
+ { type: 'patch', ref: 'users', text: String(users) },
743
+ { type: 'patch', ref: 'orders', text: String(orders) },
744
+ { type: 'patch', ref: 'revenue', text: '$' + Math.floor(Math.random() * 10000) }
707
745
  ]);
708
746
  }, 1000);
709
747
  });
710
748
  ```
711
749
 
712
- ### Chat with Auto-scroll (append + register + call)
750
+ ### Chat with Auto-scroll (append + call)
713
751
 
714
752
  ```javascript
715
753
  app.page('/', function(client) {
716
- client.render('#app', {
754
+ client.mount('#app', {
717
755
  t: 'div', c: [
718
756
  { t: 'div', a: { id: 'chat', style: 'max-height:400px;overflow-y:auto' } },
719
757
  { t: 'div', a: { style: 'display:flex;gap:8px' }, c: [
720
758
  { t: 'input', a: { type: 'text', id: 'msg-input' } },
721
- { t: 'button', a: { 'data-bw-action': 'send' }, c: 'Send' }
759
+ { t: 'button', a: { class: 'bw_act_send' }, c: 'Send' }
722
760
  ]}
723
761
  ]
724
762
  });
725
763
 
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
764
  client.on('send', function(data) {
731
765
  if (!data.inputValue) return;
732
766
 
@@ -734,7 +768,7 @@ app.page('/', function(client) {
734
768
  client.append('#chat', {
735
769
  t: 'div', a: { style: 'padding:4px' }, c: data.inputValue
736
770
  });
737
- client.call('scrollChat');
771
+ client.call('scrollTo', '#chat');
738
772
 
739
773
  // Focus back on the input
740
774
  client.call('focus', '#msg-input');
@@ -784,7 +818,7 @@ Once connected, you get a REPL:
784
818
  bw> document.title
785
819
  "My Page"
786
820
 
787
- bw> bw.$('.bw-card').length
821
+ bw> bw.$('.bw_bccl_card').length
788
822
  3
789
823
 
790
824
  bw> /tree #app 2
@@ -812,7 +846,6 @@ Saved: page.png (1440x900, 245832 bytes)
812
846
 
813
847
  | Command | Description |
814
848
  |---------|-------------|
815
- | `<expression>` | Evaluate JS in the browser (e.g., `document.title`) |
816
849
  | `/tree [sel] [depth]` | DOM tree summary (default: body, depth 3) |
817
850
  | `/screenshot [sel] [file]` | Capture element to PNG (requires `--allow-screenshot`) |
818
851
  | `/mount <sel> <comp> [json]` | Mount a BCCL component |
@@ -820,22 +853,21 @@ Saved: page.png (1440x900, 245832 bytes)
820
853
  | `/patch <id> <content>` | Update element text by ID |
821
854
  | `/listen <sel> <event>` | Watch for DOM events |
822
855
  | `/unlisten <sel> <event>` | Stop watching events |
823
- | `/exec <code>` | Execute JS (fire-and-forget) |
824
856
  | `/clients` | List connected clients |
825
857
  | `/help`, `/quit` | Help / exit |
826
858
 
827
859
  ### Security
828
860
 
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.**
861
+ Attach mode is a debugging tool. The server binds to `localhost` by default. **Never expose to the public internet.**
830
862
 
831
863
  For the full attach guide, see [bwcli attach documentation](bw-attach.md).
832
864
 
833
865
  ## Related
834
866
 
835
- - [Protocol Reference Page](../pages/12-bwserve-protocol.html) — Interactive protocol reference with all 9 message types
867
+ - [Protocol Reference Page](../pages/12-bwserve-protocol.html) — Interactive protocol reference with all 13 message types
836
868
  - [Sandbox](../pages/14-bwserve-sandbox.html) — Try bwserve protocol in the browser (no server needed)
837
869
  - [Screenshot Example](../examples/client-server/screenshot-server.js) — Runnable screenshot demo
838
- - [Design Document](../dev/bw-client-server.md) — Protocol design decisions and architecture
870
+ - [Design Document](../dev/archive/bw-client-server.md) — Original (pre-v2.1) protocol design exploration, kept for history
839
871
  - [CLI](cli.md) — The `bwcli` command for file conversion and pipe server
840
872
  - [Attach Mode](bw-attach.md) — Full remote debugging REPL documentation
841
873
  - [Embedded Tutorial](tutorial-embedded.md) — ESP32 IoT dashboard with C macros and r-prefix JSON