bitwrench 2.0.22 → 2.0.24
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/LICENSE.txt +1 -1
- package/README.md +4 -3
- package/bin/bwmcp.js +3 -0
- package/dist/bitwrench-bccl.cjs.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +3 -3
- package/dist/bitwrench-lean.cjs.min.js +2 -2
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +3 -3
- package/dist/bitwrench-lean.es5.min.js +2 -2
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +3 -3
- package/dist/bitwrench-lean.esm.min.js +2 -2
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +3 -3
- package/dist/bitwrench-lean.umd.min.js +2 -2
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-css.cjs.js +1 -1
- package/dist/bitwrench-util-css.cjs.min.js +1 -1
- package/dist/bitwrench-util-css.es5.js +1 -1
- package/dist/bitwrench-util-css.es5.min.js +1 -1
- package/dist/bitwrench-util-css.esm.js +1 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -1
- package/dist/bitwrench-util-css.umd.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +3 -3
- package/dist/bitwrench.cjs.min.js +2 -2
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +1 -1
- package/dist/bitwrench.es5.js +3 -3
- package/dist/bitwrench.es5.min.js +2 -2
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +3 -3
- package/dist/bitwrench.esm.min.js +2 -2
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.umd.js +3 -3
- package/dist/bitwrench.umd.min.js +2 -2
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +65 -65
- package/dist/bwserve.cjs.js +2 -2
- package/dist/bwserve.esm.js +2 -2
- package/dist/sri.json +45 -45
- package/docs/README.md +76 -0
- package/docs/app-patterns.md +264 -0
- package/docs/bitwrench-mcp.md +426 -0
- package/docs/bitwrench_api.md +2232 -0
- package/docs/bw-attach.md +399 -0
- package/docs/bwserve.md +841 -0
- package/docs/cli.md +307 -0
- package/docs/component-cheatsheet.md +144 -0
- package/docs/component-library.md +1099 -0
- package/docs/framework-translation-table.md +33 -0
- package/docs/llm-bitwrench-guide.md +672 -0
- package/docs/routing.md +562 -0
- package/docs/state-management.md +767 -0
- package/docs/taco-format.md +373 -0
- package/docs/theming.md +309 -0
- package/docs/thinking-in-bitwrench.md +1457 -0
- package/docs/tutorial-bwserve.md +297 -0
- package/docs/tutorial-embedded.md +314 -0
- package/docs/tutorial-website.md +255 -0
- package/package.json +11 -3
- package/readme.html +5 -4
- package/src/mcp/knowledge.js +231 -0
- package/src/mcp/live.js +226 -0
- package/src/mcp/server.js +216 -0
- package/src/mcp/tools.js +369 -0
- package/src/mcp/transport.js +55 -0
- package/src/version.js +3 -3
package/docs/bwserve.md
ADDED
|
@@ -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
|