bee-ladybug 0.1.0 → 0.3.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antonio Prosperi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,5 +1,8 @@
1
+
1
2
  # BeeLadybug
2
3
 
4
+
5
+
3
6
  Universal telemetry and visual inspection overlay.
4
7
 
5
8
  BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
@@ -27,22 +30,24 @@ funziona sopra un gioco, sopra una pagina, o da solo in una sandbox.
27
30
  bee-ladybug/
28
31
  ├── src/
29
32
  │ ├── index.js # API pubblica
33
+ │ ├── index.d.ts # tipi npm / tsc
30
34
  │ ├── core/
31
- │ │ ├── BeeLadybugCore.js # stato, clock, sendData
32
- │ │ └── UIOverlay.js # console scura + sparkline
35
+ │ │ ├── BeeLadybugCore.js
36
+ │ │ └── UIOverlay.js
33
37
  │ └── adapters/
34
- │ ├── CanvasAdapter.js # hitbox / FPS / collisioni Canvas 2D
35
- │ ├── WebDOMAdapter.js # box model / hover / mutazioni HTML
36
- │ └── python/
37
- │ └── PythonBridge.js # futuro: WebSocket → sendData
38
+ │ ├── CanvasAdapter.js
39
+ │ ├── WebDOMAdapter.js
40
+ │ └── python/PythonBridge.js
38
41
  ├── examples/
39
- │ ├── core-sandbox.js # demo senza adapter
40
42
  │ ├── core.html
41
- │ ├── canvas-sandbox.js # demo CanvasAdapter
42
- │ ├── dom-sandbox.js
43
- │ └── dom.html
44
- ├── index.html
43
+ │ ├── canvas-sandbox.js
44
+ │ ├── dom.html
45
+ │ ├── python.html
46
+ │ └── python/sender.py
47
+ ├── tsconfig.json
45
48
  ├── package.json
49
+ ├── LICENSE
50
+ ├── index.html
46
51
  └── README.md
47
52
  ```
48
53
 
@@ -111,6 +116,8 @@ bee.subscribe((packet) => {
111
116
  });
112
117
  ```
113
118
 
119
+ ![Overlay Core: la ⓘ accanto a LIVE spiega che il freeze ferma solo il clock fittizio](docs/overlay-core.jpg)
120
+
114
121
  ## Tasti
115
122
 
116
123
  | Tasto | Azione |
@@ -165,6 +172,99 @@ adapter.attach();
165
172
 
166
173
  Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
167
174
 
175
+ ## PythonBridge
176
+
177
+ Python non vede l'overlay. Manda JSON. Il browser lo srotola in `sendData()`.
178
+
179
+ ```js
180
+ import { BeeLadybugCore, PythonBridge } from './src/index.js';
181
+
182
+ const bee = new BeeLadybugCore();
183
+ const bridge = new PythonBridge(bee, { url: 'ws://127.0.0.1:8765' });
184
+ bridge.connect();
185
+
186
+ // stesso parser, senza socket:
187
+ bridge.ingest({ type: 'metric', payload: { name: 'loss', value: 0.12 } });
188
+ ```
189
+
190
+ Forma del filo:
191
+
192
+ ```json
193
+ { "type": "log", "payload": { "message": "epoch 3" } }
194
+ { "type": "error", "message": "CUDA OOM" }
195
+ ```
196
+
197
+ Sender di esempio (solo stdlib, nessun pip):
198
+
199
+ ```bash
200
+ python examples/python/sender.py
201
+ ```
202
+
203
+ Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
204
+
205
+ ![Overlay PythonBridge: ASK AI e quadratini mock/echo per scegliere il provider](docs/overlay-python.jpg)
206
+
207
+ ## npm / TypeScript
208
+
209
+ ```bash
210
+ npm install
211
+ npm run typecheck
212
+ ```
213
+
214
+ ```js
215
+ import { BeeLadybugCore } from 'bee-ladybug';
216
+ ```
217
+
218
+ Il runtime resta JavaScript ESM. I tipi stanno in `src/index.d.ts` (`package.json` → `"types"`).
219
+ Keyword e repository puntano a **BeeLadybug-universal**, non a BeeEngine.
220
+
221
+ ```bash
222
+ npm run typecheck
223
+ npm run lint
224
+ ```
225
+
226
+ ```bash
227
+ npm login
228
+ npm publish --access public
229
+ ```
230
+
231
+ Versione runtime e pacchetto: **0.3.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
232
+
233
+ ## Assistente AI (gancio, nessun vendor)
234
+
235
+ BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta i propri modelli. `registerAssistant` **aggiunge** (non sostituisce gli altri). `setActiveAssistant(name)` sceglie chi risponde a **ASK AI**.
236
+
237
+ ```js
238
+ bee.registerAssistant({
239
+ name: 'ollama',
240
+ async complete({ question, snapshot }) {
241
+ const res = await fetch('http://127.0.0.1:11434/api/generate', {
242
+ method: 'POST',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify({
245
+ model: 'llama3',
246
+ prompt: `${question}\n${JSON.stringify(snapshot)}`,
247
+ stream: false
248
+ })
249
+ });
250
+ const data = await res.json();
251
+ return data.response;
252
+ }
253
+ });
254
+
255
+ bee.registerAssistant({
256
+ name: 'mock',
257
+ complete({ snapshot }) {
258
+ return `logs=${snapshot.logs.length}`;
259
+ }
260
+ });
261
+
262
+ bee.setActiveAssistant('ollama');
263
+ await bee.ask('perché il loss sale?');
264
+ ```
265
+
266
+ Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI** e un quadratino per ogni provider; quello attivo è evidenziato. Senza provider, il bottone spiega come collegarlo. `setAssistant` resta come scorciatoia: registra e attiva, senza cancellare gli altri.
267
+
168
268
  ## API Core
169
269
 
170
270
  ```js
@@ -190,14 +290,16 @@ bee.getHud();
190
290
  bee.show(); bee.hide(); bee.toggle();
191
291
  bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
192
292
  bee.applySlowMo(); bee.restoreRealtime();
293
+ bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
294
+ bee.setActiveAssistant('mock');
295
+ await bee.ask('why fps drop?');
193
296
  bee.destroy();
194
297
  ```
195
298
 
196
299
  ## Roadmap
197
300
 
198
- 1. **Ora** — Core + overlay + CanvasAdapter + WebDOMAdapter. Fatto.
199
- 2. **PythonBridge** — WebSocket JSON `{ type, payload }` verso `sendData`.
200
- 3. **Pacchetto npm** — `import { BeeLadybugCore } from 'bee-ladybug'`.
301
+ 1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
302
+ 2. **npm 0.3.0** — `npm publish`.
201
303
 
202
304
  ## Come contribuire
203
305
 
package/package.json CHANGED
@@ -1,23 +1,56 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.1.0",
4
- "description": "Universal telemetry and visual inspection overlay for Canvas, DOM, bots, Python and AI flows.",
3
+ "version": "0.3.0",
4
+ "description": "Universal telemetry and visual inspection overlay for Canvas, DOM, Python and AI flows.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "module": "src/index.js",
8
+ "types": "src/index.d.ts",
8
9
  "exports": {
9
- ".": "./src/index.js"
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "import": "./src/index.js",
13
+ "default": "./src/index.js"
14
+ }
10
15
  },
11
16
  "files": [
12
17
  "src"
13
18
  ],
14
- "license": "MIT",
19
+ "scripts": {
20
+ "typecheck": "tsc --noEmit",
21
+ "lint": "eslint src examples",
22
+ "test": "node --test tests/**/*.test.js"
23
+ },
15
24
  "keywords": [
16
25
  "debug",
17
26
  "telemetry",
18
27
  "overlay",
19
28
  "observability",
29
+ "devtools",
20
30
  "canvas",
21
- "devtools"
22
- ]
23
- }
31
+ "dom",
32
+ "python",
33
+ "websocket",
34
+ "ai",
35
+ "esm"
36
+ ],
37
+ "author": "Antonio Prosperi",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/antonioprosperi2-svg/BeeLadybug-universal.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal/issues"
45
+ },
46
+ "homepage": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal#readme",
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "^9.36.0",
52
+ "eslint": "^10.8.1",
53
+ "globals": "^16.0.0",
54
+ "typescript": "^5.7.0"
55
+ }
56
+ }
@@ -1,40 +1,242 @@
1
1
  /**
2
- * PythonBridge — future inbound channel for Python / AI processes.
2
+ * PythonBridge — inbound channel for Python / AI / headless processes.
3
3
  *
4
- * Planned transport: WebSocket (or HTTP ingest) that unpacks JSON packets
5
- * and calls core.sendData(type, payload). The core stays browser-side;
6
- * Python never talks to the overlay directly.
4
+ * Speaks WebSocket JSON and calls core.sendData(type, payload).
5
+ * The core stays browser-side; Python never talks to the overlay.
7
6
  *
8
- * Expected wire shape:
9
- * { "type": "log"|"error"|"metric"|"fps"|"state", "payload": { ... } }
7
+ * Wire shape (either form):
8
+ * { "type": "metric", "payload": { "name": "loss", "value": 0.12 } }
9
+ * { "type": "log", "message": "epoch 3" }
10
+ *
11
+ * ingest() is the same parser used by the socket, so tests and the mock
12
+ * demo do not need a live Python process.
10
13
  */
14
+
15
+ export const PYTHON_BRIDGE_DEFAULTS = Object.freeze({
16
+ url: 'ws://127.0.0.1:8765',
17
+ autoReconnect: true,
18
+ reconnectMs: 1500,
19
+ source: 'python'
20
+ });
21
+
11
22
  export class PythonBridge {
12
23
  /**
13
24
  * @param {import('../../core/BeeLadybugCore.js').BeeLadybugCore} core
14
- * @param {{ url?: string }} [options]
25
+ * @param {Partial<typeof PYTHON_BRIDGE_DEFAULTS>} [options]
15
26
  */
16
27
  constructor(core, options = {}) {
28
+ const cfg = { ...PYTHON_BRIDGE_DEFAULTS, ...options };
17
29
  this.core = core;
18
- this.url = options.url ?? 'ws://127.0.0.1:8765';
30
+ this.url = cfg.url;
31
+ this.autoReconnect = cfg.autoReconnect;
32
+ this.reconnectMs = cfg.reconnectMs;
33
+ this.source = cfg.source;
34
+ this.status = 'idle';
35
+
19
36
  this.#socket = null;
37
+ this.#wanted = false;
38
+ this.#timer = 0;
39
+ this.#packets = 0;
40
+ this.#retries = 0;
20
41
  }
21
42
 
22
43
  #socket;
44
+ #wanted;
45
+ #timer;
46
+ #packets;
47
+ #retries;
23
48
 
24
- connect() {
25
- this.core?.sendData('log', {
26
- source: 'python',
27
- level: 'warn',
28
- message: `PythonBridge reserved (${this.url}). WebSocket ingest comes after adapters.`
29
- });
49
+ get connected() {
50
+ return this.status === 'open';
51
+ }
52
+
53
+ connect(url) {
54
+ if (url) this.url = url;
55
+ this.#wanted = true;
56
+ this.#retries = 0;
57
+ this.#open();
30
58
  return this;
31
59
  }
32
60
 
33
61
  disconnect() {
62
+ this.#wanted = false;
63
+ this.#clearTimer();
34
64
  if (this.#socket) {
35
- this.#socket.close();
65
+ try {
66
+ this.#socket.close();
67
+ } catch {
68
+ /* already closing */
69
+ }
36
70
  this.#socket = null;
37
71
  }
72
+ this.#setStatus('idle');
73
+ this.core?.sendData('log', {
74
+ source: this.source,
75
+ message: 'PythonBridge disconnected.'
76
+ });
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * Parse one wire message (object, JSON string, or array) into sendData().
82
+ * @param {unknown} raw
83
+ */
84
+ ingest(raw) {
85
+ if (!this.core) return this;
86
+ if (raw == null) return this;
87
+
88
+ let data = raw;
89
+ if (typeof raw === 'string') {
90
+ const text = raw.trim();
91
+ if (!text) return this;
92
+ try {
93
+ data = JSON.parse(text);
94
+ } catch {
95
+ this.core.sendData('error', {
96
+ source: this.source,
97
+ message: `bad JSON: ${text.slice(0, 96)}`
98
+ });
99
+ return this;
100
+ }
101
+ }
102
+
103
+ if (Array.isArray(data)) {
104
+ for (let i = 0; i < data.length; i++) this.ingest(data[i]);
105
+ return this;
106
+ }
107
+
108
+ if (typeof data !== 'object') return this;
109
+
110
+ const type = data.type || data.channel;
111
+ if (typeof type !== 'string' || !type) {
112
+ this.core.sendData('warn', {
113
+ source: this.source,
114
+ message: 'packet missing type'
115
+ });
116
+ return this;
117
+ }
118
+
119
+ const payload = unpackPayload(data, this.source);
120
+ this.#packets += 1;
121
+ this.core.sendData(type, payload);
122
+ this.core.sendData('state', {
123
+ source: this.source,
124
+ key: 'py.packets',
125
+ value: this.#packets
126
+ });
38
127
  return this;
39
128
  }
129
+
130
+ #open() {
131
+ if (!this.#wanted) return;
132
+ if (typeof WebSocket === 'undefined') {
133
+ this.core?.sendData('error', {
134
+ source: this.source,
135
+ message: 'WebSocket API missing in this runtime.'
136
+ });
137
+ this.#setStatus('closed');
138
+ return;
139
+ }
140
+
141
+ this.#clearTimer();
142
+ if (this.#socket) {
143
+ try {
144
+ this.#socket.close();
145
+ } catch {
146
+ /* ignore */
147
+ }
148
+ }
149
+
150
+ this.#setStatus('connecting');
151
+ if (this.#retries === 0) {
152
+ this.core?.sendData('log', {
153
+ source: this.source,
154
+ message: `PythonBridge connecting ${this.url}`
155
+ });
156
+ }
157
+
158
+ let socket;
159
+ try {
160
+ socket = new WebSocket(this.url);
161
+ } catch (err) {
162
+ this.core?.sendData('error', {
163
+ source: this.source,
164
+ message: `connect failed: ${err instanceof Error ? err.message : String(err)}`
165
+ });
166
+ this.#setStatus('closed');
167
+ this.#scheduleReconnect();
168
+ return;
169
+ }
170
+
171
+ this.#socket = socket;
172
+ socket.addEventListener('open', () => {
173
+ if (socket !== this.#socket) return;
174
+ this.#setStatus('open');
175
+ this.#retries = 0;
176
+ this.core?.sendData('log', {
177
+ source: this.source,
178
+ message: `PythonBridge open (${this.url})`
179
+ });
180
+ });
181
+ socket.addEventListener('message', (event) => {
182
+ if (socket !== this.#socket) return;
183
+ this.ingest(event.data);
184
+ });
185
+ socket.addEventListener('close', () => {
186
+ if (socket !== this.#socket) return;
187
+ this.#socket = null;
188
+ this.#setStatus('closed');
189
+ if (this.#retries === 0) {
190
+ this.core?.sendData('warn', {
191
+ source: this.source,
192
+ message: 'PythonBridge socket closed.'
193
+ });
194
+ }
195
+ this.#retries += 1;
196
+ this.#scheduleReconnect();
197
+ });
198
+ socket.addEventListener('error', () => {
199
+ if (this.#retries > 0) return;
200
+ this.core?.sendData('error', {
201
+ source: this.source,
202
+ message: `PythonBridge socket error (${this.url})`
203
+ });
204
+ });
205
+ }
206
+
207
+ #scheduleReconnect() {
208
+ if (!this.#wanted || !this.autoReconnect) return;
209
+ this.#clearTimer();
210
+ this.#timer = setTimeout(() => this.#open(), this.reconnectMs);
211
+ }
212
+
213
+ #clearTimer() {
214
+ if (this.#timer && typeof clearTimeout === 'function') clearTimeout(this.#timer);
215
+ this.#timer = 0;
216
+ }
217
+
218
+ #setStatus(status) {
219
+ this.status = status;
220
+ this.core?.sendData('state', {
221
+ source: this.source,
222
+ key: 'py.status',
223
+ value: status
224
+ });
225
+ }
226
+ }
227
+
228
+ function unpackPayload(data, source) {
229
+ const body = data.payload != null && typeof data.payload === 'object' && !Array.isArray(data.payload)
230
+ ? { ...data.payload }
231
+ : omitType(data);
232
+ if (!body.source) body.source = source;
233
+ return body;
234
+ }
235
+
236
+ function omitType(data) {
237
+ const out = { ...data };
238
+ delete out.type;
239
+ delete out.channel;
240
+ delete out.payload;
241
+ return out;
40
242
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Vendor-agnostic AI hook. BeeLadybug never ships an API key or a model:
3
+ * the host app injects complete({ question, snapshot }).
4
+ */
5
+
6
+ const SECRET_KEY = /key|token|secret|password|authorization|cookie|apikey/i;
7
+
8
+ /**
9
+ * @param {import('./BeeLadybugCore.js').BeeLadybugCore} core
10
+ * @param {string} question
11
+ */
12
+ export function buildAssistantSnapshot(core, question) {
13
+ const logs = core.getLogs().slice(-40).map((packet) => ({
14
+ type: packet.type,
15
+ level: packet.level,
16
+ source: packet.source,
17
+ message: String(packet.payload?.message ?? packet.payload?.value ?? packet.type),
18
+ frame: packet.frame
19
+ }));
20
+ const history = core.getPrimaryHistory();
21
+ return {
22
+ question: String(question || ''),
23
+ version: core.version,
24
+ state: core.getState(),
25
+ hud: scrub(core.getHud()),
26
+ logs,
27
+ history: {
28
+ name: history.name,
29
+ min: history.min,
30
+ max: history.max,
31
+ avg: history.avg,
32
+ last: history.last,
33
+ samples: history.values.slice(-24)
34
+ }
35
+ };
36
+ }
37
+
38
+ export function normalizeAssistant(provider) {
39
+ if (!provider || typeof provider.complete !== 'function') return null;
40
+ const name = typeof provider.name === 'string' && provider.name
41
+ ? provider.name
42
+ : 'custom';
43
+ return { name, complete: provider.complete.bind(provider) };
44
+ }
45
+
46
+ function scrub(value, depth = 0) {
47
+ if (value == null || typeof value !== 'object') {
48
+ if (typeof value === 'string' && value.length > 400) return `${value.slice(0, 400)}…`;
49
+ return value;
50
+ }
51
+ if (depth > 4) return '[…]';
52
+ if (Array.isArray(value)) {
53
+ return value.slice(0, 24).map((item) => scrub(item, depth + 1));
54
+ }
55
+ const out = {};
56
+ for (const key of Object.keys(value)) {
57
+ out[key] = SECRET_KEY.test(key) ? '[redacted]' : scrub(value[key], depth + 1);
58
+ }
59
+ return out;
60
+ }
@@ -7,8 +7,9 @@
7
7
  */
8
8
 
9
9
  import { UIOverlay } from './UIOverlay.js';
10
+ import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
10
11
 
11
- export const BEE_LADYBUG_VERSION = '0.1.0';
12
+ export const BEE_LADYBUG_VERSION = '0.3.0';
12
13
 
13
14
  export const CORE_DEFAULTS = Object.freeze({
14
15
  toggleKey: 'F2',
@@ -92,6 +93,9 @@ export class BeeLadybugCore {
92
93
  this.#onKeyDown = (event) => this.#handleKey(event);
93
94
  this.#onTick = (now) => this.#tick(now);
94
95
  this.#started = false;
96
+ this.#assistants = new Map();
97
+ this.#activeAssistant = null;
98
+ this.#askBusy = false;
95
99
 
96
100
  this.ui = null;
97
101
  if (cfg.ui !== false && typeof document !== 'undefined') {
@@ -121,6 +125,11 @@ export class BeeLadybugCore {
121
125
  #onKeyDown;
122
126
  #onTick;
123
127
  #started;
128
+ /** @type {Map<string, { name: string, complete: Function }>} */
129
+ #assistants;
130
+ /** @type {string | null} */
131
+ #activeAssistant;
132
+ #askBusy;
124
133
 
125
134
  /**
126
135
  * Public adapter contract. `type` is a channel name; `payload` is raw data.
@@ -153,6 +162,153 @@ export class BeeLadybugCore {
153
162
  return () => this.#listeners.delete(handler);
154
163
  }
155
164
 
165
+ get hasAssistant() {
166
+ return Boolean(this.#activeProvider());
167
+ }
168
+
169
+ get assistantBusy() {
170
+ return this.#askBusy;
171
+ }
172
+
173
+ /** Registered provider names, insertion order. */
174
+ getAssistants() {
175
+ return [...this.#assistants.keys()];
176
+ }
177
+
178
+ /**
179
+ * Add a model without dropping the others. Same `name` updates that slot.
180
+ * The first registration becomes active; later ones do not steal focus.
181
+ * @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
182
+ */
183
+ registerAssistant(provider) {
184
+ return this.#putAssistant(provider, { activate: !this.#activeAssistant, via: 'registerAssistant' });
185
+ }
186
+
187
+ /**
188
+ * @param {string} name
189
+ */
190
+ setActiveAssistant(name) {
191
+ const key = String(name || '');
192
+ if (!this.#assistants.has(key)) {
193
+ this.sendData('warn', {
194
+ source: 'ai',
195
+ message: `No assistant named "${key}". bee.registerAssistant({ name, complete }) first.`
196
+ });
197
+ return this;
198
+ }
199
+ this.#activeAssistant = key;
200
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: key });
201
+ this.sendData('log', { source: 'ai', message: `assistant active (${key})` });
202
+ return this;
203
+ }
204
+
205
+ /**
206
+ * Register and activate one provider. Does not remove the others.
207
+ * @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
208
+ */
209
+ setAssistant(provider) {
210
+ return this.#putAssistant(provider, { activate: true, via: 'setAssistant' });
211
+ }
212
+
213
+ /**
214
+ * @param {string} [name] omit to drop every provider
215
+ */
216
+ clearAssistant(name) {
217
+ if (name == null || name === '') {
218
+ this.#assistants.clear();
219
+ this.#activeAssistant = null;
220
+ } else {
221
+ const key = String(name);
222
+ this.#assistants.delete(key);
223
+ if (this.#activeAssistant === key) {
224
+ this.#activeAssistant = this.#assistants.keys().next().value ?? null;
225
+ }
226
+ }
227
+ this.sendData('state', {
228
+ source: 'ai',
229
+ key: 'ai.assist',
230
+ value: this.#activeAssistant ?? 'off'
231
+ });
232
+ return this;
233
+ }
234
+
235
+ /**
236
+ * Send recent telemetry to the active assistant and print the answer.
237
+ * @param {string} [question]
238
+ * @returns {Promise<string|null>}
239
+ */
240
+ async ask(question = 'Diagnose the current telemetry.') {
241
+ const assistant = this.#activeProvider();
242
+ if (!assistant) {
243
+ this.sendData('warn', {
244
+ source: 'ai',
245
+ message: 'No assistant. bee.registerAssistant({ name, complete }) then ASK AI.'
246
+ });
247
+ return null;
248
+ }
249
+ if (this.#askBusy) {
250
+ this.sendData('warn', { source: 'ai', message: 'assistant busy' });
251
+ return null;
252
+ }
253
+ const q = String(question || 'Diagnose the current telemetry.');
254
+ this.#askBusy = true;
255
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: 'thinking' });
256
+ this.sendData('log', { source: 'ai', message: `ask: ${q}` });
257
+ try {
258
+ const snapshot = buildAssistantSnapshot(this, q);
259
+ const raw = await assistant.complete({ question: q, snapshot });
260
+ const text = String(raw ?? '').trim() || '(empty answer)';
261
+ this.sendData('log', { source: 'ai', message: text });
262
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: assistant.name });
263
+ return text;
264
+ } catch (err) {
265
+ this.sendData('error', {
266
+ source: 'ai',
267
+ message: err instanceof Error ? err.message : String(err)
268
+ });
269
+ this.sendData('state', {
270
+ source: 'ai',
271
+ key: 'ai.assist',
272
+ value: assistant?.name ?? 'error'
273
+ });
274
+ return null;
275
+ } finally {
276
+ this.#askBusy = false;
277
+ }
278
+ }
279
+
280
+ #activeProvider() {
281
+ return this.#activeAssistant ? this.#assistants.get(this.#activeAssistant) ?? null : null;
282
+ }
283
+
284
+ /**
285
+ * @param {{ name?: string, complete: Function }} provider
286
+ * @param {{ activate: boolean, via: string }} opts
287
+ */
288
+ #putAssistant(provider, opts) {
289
+ const next = normalizeAssistant(provider);
290
+ if (!next) {
291
+ this.sendData('warn', {
292
+ source: 'ai',
293
+ message: `${opts.via}() needs { complete(ctx) }. No vendor is bundled.`
294
+ });
295
+ return this;
296
+ }
297
+ this.#assistants.set(next.name, next);
298
+ if (opts.activate) this.#activeAssistant = next.name;
299
+ this.sendData('state', {
300
+ source: 'ai',
301
+ key: 'ai.assist',
302
+ value: this.#activeAssistant ?? next.name
303
+ });
304
+ this.sendData('log', {
305
+ source: 'ai',
306
+ message: `assistant ready (${next.name})`
307
+ });
308
+ return this;
309
+ }
310
+
311
+
156
312
  start() {
157
313
  if (this.#started) return this;
158
314
  this.#started = true;
@@ -194,6 +350,9 @@ export class BeeLadybugCore {
194
350
  this.#history.clear();
195
351
  this.#hud.clear();
196
352
  this.#latest.clear();
353
+ this.#assistants.clear();
354
+ this.#activeAssistant = null;
355
+ this.#askBusy = false;
197
356
  this.ui?.destroy();
198
357
  this.ui = null;
199
358
  return this;
@@ -219,6 +378,10 @@ export class BeeLadybugCore {
219
378
  return this.visible ? this.hide() : this.show();
220
379
  }
221
380
 
381
+ /**
382
+ * Stops the simulated clock only. Overlay RAF keeps running.
383
+ * Adapters must listen for `sys` packets with `op: 'freeze'`.
384
+ */
222
385
  freeze() {
223
386
  if (this.frozen) return this;
224
387
  this.frozen = true;
@@ -276,7 +439,9 @@ export class BeeLadybugCore {
276
439
  coreFps: this.coreFps,
277
440
  logCount: this.#logs.length,
278
441
  hud: this.getHud(),
279
- mode: this.#modeLabel()
442
+ mode: this.#modeLabel(),
443
+ assistant: this.#activeAssistant,
444
+ assistants: this.getAssistants()
280
445
  };
281
446
  }
282
447
 
@@ -99,8 +99,14 @@ const STYLES = `
99
99
  border: 1px solid rgba(110, 231, 255, 0.35);
100
100
  padding: 1px 5px;
101
101
  }
102
- .bl-mode {
102
+ .bl-mode-wrap {
103
103
  margin-left: auto;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ gap: 5px;
107
+ position: relative;
108
+ }
109
+ .bl-mode {
104
110
  font-size: 10px;
105
111
  font-weight: 700;
106
112
  letter-spacing: 0.08em;
@@ -108,6 +114,50 @@ const STYLES = `
108
114
  }
109
115
  .bl-mode[data-mode="FREEZE"] { color: #ff3b4e; }
110
116
  .bl-mode[data-mode^="SLOW"] { color: #ffd36a; }
117
+ .bl-info {
118
+ appearance: none;
119
+ position: relative;
120
+ flex: 0 0 auto;
121
+ width: 15px;
122
+ height: 15px;
123
+ padding: 0;
124
+ border: 1px solid rgba(139, 145, 163, 0.7);
125
+ border-radius: 50%;
126
+ background: transparent;
127
+ color: #8b91a3;
128
+ font: 700 9px ui-monospace, Consolas, monospace;
129
+ line-height: 1;
130
+ cursor: help;
131
+ }
132
+ .bl-info:hover,
133
+ .bl-info:focus-visible {
134
+ color: #ffd36a;
135
+ border-color: #ffd36a;
136
+ outline: none;
137
+ }
138
+ .bl-info-tip {
139
+ display: none;
140
+ position: absolute;
141
+ top: calc(100% + 6px);
142
+ right: 0;
143
+ width: 228px;
144
+ padding: 8px 9px;
145
+ background: #0c0e14;
146
+ border: 1px solid rgba(255, 211, 106, 0.45);
147
+ color: #e9edf5;
148
+ font: 400 10px/1.45 ui-monospace, Consolas, monospace;
149
+ letter-spacing: 0;
150
+ text-transform: none;
151
+ z-index: 3;
152
+ pointer-events: none;
153
+ white-space: normal;
154
+ text-align: left;
155
+ }
156
+ .bl-info:hover .bl-info-tip,
157
+ .bl-info:focus .bl-info-tip,
158
+ .bl-info:focus-visible .bl-info-tip {
159
+ display: block;
160
+ }
111
161
  .bl-stats {
112
162
  display: grid;
113
163
  grid-template-columns: 1fr 1fr;
@@ -169,6 +219,7 @@ const STYLES = `
169
219
  .bl-foot {
170
220
  display: flex;
171
221
  align-items: center;
222
+ flex-wrap: wrap;
172
223
  gap: 6px;
173
224
  padding: 8px 10px;
174
225
  border-top: 1px solid rgba(255, 59, 78, 0.22);
@@ -188,6 +239,31 @@ const STYLES = `
188
239
  border-color: #ff3b4e;
189
240
  color: #fff;
190
241
  }
242
+ .bl-providers {
243
+ display: inline-flex;
244
+ align-items: center;
245
+ gap: 3px;
246
+ }
247
+ .bl-providers:empty {
248
+ display: none;
249
+ }
250
+ .bl-provider {
251
+ appearance: none;
252
+ width: 22px;
253
+ height: 22px;
254
+ padding: 0;
255
+ background: #16181f;
256
+ color: #fff6d8;
257
+ border: 1px solid rgba(255, 211, 106, 0.45);
258
+ font: 700 8px ui-monospace, Consolas, monospace;
259
+ letter-spacing: 0.04em;
260
+ cursor: pointer;
261
+ }
262
+ .bl-provider[data-hot="true"] {
263
+ background: #6ee7ff;
264
+ border-color: #6ee7ff;
265
+ color: #05060a;
266
+ }
191
267
  .bl-hint {
192
268
  margin-left: auto;
193
269
  font-size: 9px;
@@ -205,22 +281,23 @@ export class UIOverlay {
205
281
  */
206
282
  constructor(core, options = {}) {
207
283
  this.core = core;
208
- this.#options = options;
284
+ void options;
209
285
  this.#host = null;
210
286
  this.#root = null;
211
287
  this.#els = null;
212
288
  this.#stickBottom = true;
213
289
  this.#lastLogId = 0;
214
290
  this.#mounted = false;
291
+ this.#providerSig = '';
215
292
  }
216
293
 
217
- #options;
218
294
  #host;
219
295
  #root;
220
296
  #els;
221
297
  #stickBottom;
222
298
  #lastLogId;
223
299
  #mounted;
300
+ #providerSig;
224
301
 
225
302
  /** @param {'auto' | HTMLElement} [target] */
226
303
  mount(target = 'auto') {
@@ -247,7 +324,13 @@ export class UIOverlay {
247
324
  <span class="bl-mark" aria-hidden="true"></span>
248
325
  <span class="bl-title">BEE LADYBUG</span>
249
326
  <span class="bl-badge">CORE</span>
250
- <span class="bl-mode" data-el="mode">LIVE</span>
327
+ <span class="bl-mode-wrap">
328
+ <span class="bl-mode" data-el="mode">LIVE</span>
329
+ <button class="bl-info" type="button" aria-describedby="bl-freeze-tip" aria-label="Freeze stops the simulated clock, not this overlay">
330
+ <span aria-hidden="true">ⓘ</span>
331
+ <span class="bl-info-tip" id="bl-freeze-tip">Freeze does not stop this overlay. It only stops the simulated clock. Adapters must listen for the sys event (op: freeze).</span>
332
+ </button>
333
+ </span>
251
334
  </div>
252
335
  <div class="bl-stats">
253
336
  <div>HOST <b data-el="hostFps">—</b></div>
@@ -268,6 +351,8 @@ export class UIOverlay {
268
351
  <button class="bl-btn" data-act="slow" type="button">F3 SLOW</button>
269
352
  <button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
270
353
  <button class="bl-btn" data-act="live" type="button">1x LIVE</button>
354
+ <button class="bl-btn" data-act="ask" type="button">ASK AI</button>
355
+ <span class="bl-providers" data-el="providers"></span>
271
356
  <span class="bl-hint">F2 overlay</span>
272
357
  </div>
273
358
  </div>
@@ -285,7 +370,8 @@ export class UIOverlay {
285
370
  graphName: this.#root.querySelector('[data-el="graphName"]'),
286
371
  graphStats: this.#root.querySelector('[data-el="graphStats"]'),
287
372
  hud: this.#root.querySelector('[data-el="hud"]'),
288
- console: this.#root.querySelector('[data-el="console"]')
373
+ console: this.#root.querySelector('[data-el="console"]'),
374
+ providers: this.#root.querySelector('[data-el="providers"]')
289
375
  };
290
376
 
291
377
  this.#root.querySelector('.bl-foot').addEventListener('click', (event) => {
@@ -295,6 +381,8 @@ export class UIOverlay {
295
381
  if (act === 'slow') this.core.applySlowMo();
296
382
  else if (act === 'freeze') this.core.toggleFreeze();
297
383
  else if (act === 'live') this.core.restoreRealtime();
384
+ else if (act === 'ask') this.core.ask();
385
+ else if (act === 'assist') this.core.setActiveAssistant(btn.getAttribute('data-name'));
298
386
  });
299
387
 
300
388
  this.#els.console.addEventListener('scroll', () => {
@@ -316,6 +404,7 @@ export class UIOverlay {
316
404
  this.#root = null;
317
405
  this.#els = null;
318
406
  this.#mounted = false;
407
+ this.#providerSig = '';
319
408
  return this;
320
409
  }
321
410
 
@@ -364,6 +453,7 @@ export class UIOverlay {
364
453
  this.#paintGraph(primary);
365
454
  this.#renderHud(state.hud);
366
455
  this.#syncButtons(state);
456
+ this.#syncProviders(state);
367
457
 
368
458
  if (this.#stickBottom) {
369
459
  this.#els.console.scrollTop = this.#els.console.scrollHeight;
@@ -415,6 +505,28 @@ export class UIOverlay {
415
505
  freeze.dataset.hot = state.frozen ? 'true' : 'false';
416
506
  slow.dataset.hot = !state.frozen && state.timeScale !== 1 ? 'true' : 'false';
417
507
  live.dataset.hot = !state.frozen && state.timeScale === 1 ? 'true' : 'false';
508
+ const ask = this.#root.querySelector('[data-act="ask"]');
509
+ if (ask) {
510
+ ask.dataset.hot = this.core.hasAssistant ? 'true' : 'false';
511
+ ask.disabled = Boolean(this.core.assistantBusy);
512
+ }
513
+ }
514
+
515
+ #syncProviders(state) {
516
+ const wrap = this.#els.providers;
517
+ if (!wrap) return;
518
+ const names = Array.isArray(state.assistants) ? state.assistants : [];
519
+ const active = state.assistant ?? '';
520
+ const sig = `${names.join('\0')}\0${active}`;
521
+ if (sig === this.#providerSig) return;
522
+ this.#providerSig = sig;
523
+ let html = '';
524
+ for (let i = 0; i < names.length; i++) {
525
+ const name = names[i];
526
+ const hot = name === active ? 'true' : 'false';
527
+ html += `<button class="bl-provider" data-act="assist" data-name="${escapeHtml(name)}" data-hot="${hot}" type="button" title="${escapeHtml(name)}">${escapeHtml(abbrevName(name))}</button>`;
528
+ }
529
+ wrap.innerHTML = html;
418
530
  }
419
531
 
420
532
  #paintGraph(series) {
@@ -468,6 +580,11 @@ export class UIOverlay {
468
580
  }
469
581
  }
470
582
 
583
+ function abbrevName(name) {
584
+ const s = String(name || '?');
585
+ return s.slice(0, 2).toUpperCase();
586
+ }
587
+
471
588
  function fmtHud(value) {
472
589
  if (value == null) return '—';
473
590
  if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2);
package/src/index.d.ts ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Public TypeScript surface for bee-ladybug.
3
+ * Runtime is still plain ESM JavaScript in src/*.js.
4
+ */
5
+
6
+ export const BEE_LADYBUG_VERSION: string;
7
+
8
+ export const WELL_KNOWN_TYPES: readonly [
9
+ 'fps',
10
+ 'metric',
11
+ 'coord',
12
+ 'log',
13
+ 'warn',
14
+ 'error',
15
+ 'state',
16
+ 'sys',
17
+ 'clear'
18
+ ];
19
+
20
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
21
+ export type WellKnownType = (typeof WELL_KNOWN_TYPES)[number];
22
+
23
+ export interface BeePacket {
24
+ id: number;
25
+ type: string;
26
+ payload: Record<string, unknown>;
27
+ wallTime: number;
28
+ simTime: number;
29
+ frame: number;
30
+ source: string;
31
+ level: LogLevel | null;
32
+ }
33
+
34
+ export interface HudEntry {
35
+ value: unknown;
36
+ unit?: string;
37
+ type: string;
38
+ }
39
+
40
+ export interface MetricHistory {
41
+ name: string;
42
+ values: number[];
43
+ min: number;
44
+ max: number;
45
+ avg: number;
46
+ last: number;
47
+ }
48
+
49
+ export interface CoreState {
50
+ version: string;
51
+ visible: boolean;
52
+ frozen: boolean;
53
+ timeScale: number;
54
+ simTime: number;
55
+ frameCount: number;
56
+ coreFps: number;
57
+ logCount: number;
58
+ hud: Record<string, HudEntry>;
59
+ mode: string;
60
+ assistant: string | null;
61
+ assistants: string[];
62
+ }
63
+
64
+ export interface CoreOptions {
65
+ toggleKey?: string;
66
+ slowKey?: string;
67
+ freezeKey?: string;
68
+ slowScale?: number;
69
+ maxLogs?: number;
70
+ historySize?: number;
71
+ mount?: 'auto' | false | HTMLElement;
72
+ ui?: boolean;
73
+ autoAttach?: boolean;
74
+ autoStart?: boolean;
75
+ overlay?: Record<string, unknown>;
76
+ }
77
+
78
+ export const CORE_DEFAULTS: Required<Omit<CoreOptions, 'overlay' | 'mount'>> & {
79
+ mount: 'auto';
80
+ };
81
+
82
+ export interface AssistantSnapshot {
83
+ question: string;
84
+ version: string;
85
+ state: CoreState;
86
+ hud: Record<string, HudEntry>;
87
+ logs: Array<{
88
+ type: string;
89
+ level: LogLevel | null;
90
+ source: string;
91
+ message: string;
92
+ frame: number;
93
+ }>;
94
+ history: {
95
+ name: string;
96
+ min: number;
97
+ max: number;
98
+ avg: number;
99
+ last: number;
100
+ samples: number[];
101
+ };
102
+ }
103
+
104
+ export interface AssistantProvider {
105
+ name?: string;
106
+ complete(input: { question: string; snapshot: AssistantSnapshot }): unknown;
107
+ }
108
+
109
+ export function formatSimTime(ms: number): string;
110
+
111
+ export class BeeLadybugCore {
112
+ readonly version: string;
113
+ toggleKey: string;
114
+ slowKey: string;
115
+ freezeKey: string;
116
+ slowScale: number;
117
+ maxLogs: number;
118
+ historySize: number;
119
+ visible: boolean;
120
+ frozen: boolean;
121
+ timeScale: number;
122
+ simTime: number;
123
+ frameCount: number;
124
+ coreFps: number;
125
+ ui: UIOverlay | null;
126
+
127
+ constructor(options?: CoreOptions);
128
+ sendData(type: string, payload?: unknown): BeePacket | null;
129
+ subscribe(handler: (packet: BeePacket) => void): () => void;
130
+ start(): this;
131
+ stop(): this;
132
+ attach(): this;
133
+ detach(): this;
134
+ destroy(): this;
135
+ show(): this;
136
+ hide(): this;
137
+ toggle(): this;
138
+ freeze(): this;
139
+ unfreeze(): this;
140
+ toggleFreeze(): this;
141
+ setTimeScale(scale: number): this;
142
+ applySlowMo(): this;
143
+ restoreRealtime(): this;
144
+ getState(): CoreState;
145
+ getLogs(): BeePacket[];
146
+ getHistory(name: string): MetricHistory;
147
+ getPrimaryHistory(): MetricHistory;
148
+ getHud(): Record<string, HudEntry>;
149
+ getLatest(type: string): BeePacket | null;
150
+ readonly hasAssistant: boolean;
151
+ readonly assistantBusy: boolean;
152
+ getAssistants(): string[];
153
+ registerAssistant(provider: AssistantProvider): this;
154
+ setActiveAssistant(name: string): this;
155
+ setAssistant(provider: AssistantProvider): this;
156
+ clearAssistant(name?: string): this;
157
+ ask(question?: string): Promise<string | null>;
158
+ }
159
+
160
+ export class UIOverlay {
161
+ core: BeeLadybugCore | null;
162
+ constructor(core: BeeLadybugCore, options?: Record<string, unknown>);
163
+ mount(target?: 'auto' | HTMLElement): this;
164
+ unmount(): this;
165
+ destroy(): this;
166
+ setVisible(visible: boolean): this;
167
+ onPacket(packet: BeePacket): void;
168
+ onClear(channel?: string): void;
169
+ render(): void;
170
+ }
171
+
172
+ export interface CanvasEntity {
173
+ name?: string;
174
+ label?: string;
175
+ id?: string | number;
176
+ role?: string;
177
+ primary?: boolean;
178
+ x?: number;
179
+ y?: number;
180
+ worldX?: number;
181
+ worldY?: number;
182
+ width?: number;
183
+ height?: number;
184
+ w?: number;
185
+ h?: number;
186
+ active?: boolean;
187
+ colliding?: boolean;
188
+ [key: string]: unknown;
189
+ }
190
+
191
+ export interface CanvasAdapterOptions {
192
+ canvas?: HTMLCanvasElement | null;
193
+ entities?: CanvasEntity[];
194
+ overlay?: boolean;
195
+ computeCollisions?: boolean;
196
+ colorActive?: string;
197
+ colorColliding?: string;
198
+ colorInactive?: string;
199
+ source?: string;
200
+ }
201
+
202
+ export const CANVAS_ADAPTER_DEFAULTS: Required<
203
+ Omit<CanvasAdapterOptions, 'canvas' | 'entities'>
204
+ >;
205
+
206
+ export class CanvasAdapter {
207
+ core: BeeLadybugCore | null;
208
+ canvas: HTMLCanvasElement | null;
209
+ entities: CanvasEntity[];
210
+ overlayEnabled: boolean;
211
+ computeCollisions: boolean;
212
+ frozen: boolean;
213
+ timeScale: number;
214
+ constructor(core: BeeLadybugCore, options?: CanvasAdapterOptions);
215
+ attach(): this;
216
+ detach(): this;
217
+ setCanvas(canvas: HTMLCanvasElement | null): this;
218
+ setEntities(entities: CanvasEntity[]): this;
219
+ pump(ctx?: CanvasRenderingContext2D): this;
220
+ }
221
+
222
+ export interface WebDOMAdapterOptions {
223
+ root?: ParentNode | null;
224
+ overlay?: boolean;
225
+ autoTick?: boolean;
226
+ watch?: string[];
227
+ source?: string;
228
+ colorHover?: string;
229
+ colorWatch?: string;
230
+ colorMargin?: string;
231
+ colorPadding?: string;
232
+ colorContent?: string;
233
+ }
234
+
235
+ export const WEB_DOM_ADAPTER_DEFAULTS: Required<Omit<WebDOMAdapterOptions, 'root'>>;
236
+
237
+ export class WebDOMAdapter {
238
+ core: BeeLadybugCore | null;
239
+ root: ParentNode | null;
240
+ overlayEnabled: boolean;
241
+ autoTick: boolean;
242
+ watch: string[];
243
+ frozen: boolean;
244
+ timeScale: number;
245
+ hover: Element | null;
246
+ constructor(core: BeeLadybugCore, options?: WebDOMAdapterOptions);
247
+ attach(): this;
248
+ detach(): this;
249
+ setRoot(root: ParentNode | null): this;
250
+ pump(): this;
251
+ }
252
+
253
+ export type PythonBridgeStatus = 'idle' | 'connecting' | 'open' | 'closed';
254
+
255
+ export interface PythonBridgeOptions {
256
+ url?: string;
257
+ autoReconnect?: boolean;
258
+ reconnectMs?: number;
259
+ source?: string;
260
+ }
261
+
262
+ export const PYTHON_BRIDGE_DEFAULTS: Required<PythonBridgeOptions>;
263
+
264
+ export class PythonBridge {
265
+ core: BeeLadybugCore | null;
266
+ url: string;
267
+ autoReconnect: boolean;
268
+ reconnectMs: number;
269
+ source: string;
270
+ status: PythonBridgeStatus;
271
+ readonly connected: boolean;
272
+ constructor(core: BeeLadybugCore, options?: PythonBridgeOptions);
273
+ connect(url?: string): this;
274
+ disconnect(): this;
275
+ ingest(raw: unknown): this;
276
+ }
package/src/index.js CHANGED
@@ -2,3 +2,4 @@ export { BeeLadybugCore, BEE_LADYBUG_VERSION, CORE_DEFAULTS, WELL_KNOWN_TYPES, f
2
2
  export { UIOverlay } from './core/UIOverlay.js';
3
3
  export { CanvasAdapter, CANVAS_ADAPTER_DEFAULTS } from './adapters/CanvasAdapter.js';
4
4
  export { WebDOMAdapter, WEB_DOM_ADAPTER_DEFAULTS } from './adapters/WebDOMAdapter.js';
5
+ export { PythonBridge, PYTHON_BRIDGE_DEFAULTS } from './adapters/python/PythonBridge.js';