bee-ladybug 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -27,22 +27,24 @@ funziona sopra un gioco, sopra una pagina, o da solo in una sandbox.
27
27
  bee-ladybug/
28
28
  ├── src/
29
29
  │ ├── index.js # API pubblica
30
+ │ ├── index.d.ts # tipi npm / tsc
30
31
  │ ├── core/
31
- │ │ ├── BeeLadybugCore.js # stato, clock, sendData
32
- │ │ └── UIOverlay.js # console scura + sparkline
32
+ │ │ ├── BeeLadybugCore.js
33
+ │ │ └── UIOverlay.js
33
34
  │ └── 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
35
+ │ ├── CanvasAdapter.js
36
+ │ ├── WebDOMAdapter.js
37
+ │ └── python/PythonBridge.js
38
38
  ├── examples/
39
- │ ├── core-sandbox.js # demo senza adapter
40
39
  │ ├── core.html
41
- │ ├── canvas-sandbox.js # demo CanvasAdapter
42
- │ ├── dom-sandbox.js
43
- │ └── dom.html
44
- ├── index.html
40
+ │ ├── canvas-sandbox.js
41
+ │ ├── dom.html
42
+ │ ├── python.html
43
+ │ └── python/sender.py
44
+ ├── tsconfig.json
45
45
  ├── package.json
46
+ ├── LICENSE
47
+ ├── index.html
46
48
  └── README.md
47
49
  ```
48
50
 
@@ -165,6 +167,91 @@ adapter.attach();
165
167
 
166
168
  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
169
 
170
+ ## PythonBridge
171
+
172
+ Python non vede l'overlay. Manda JSON. Il browser lo srotola in `sendData()`.
173
+
174
+ ```js
175
+ import { BeeLadybugCore, PythonBridge } from './src/index.js';
176
+
177
+ const bee = new BeeLadybugCore();
178
+ const bridge = new PythonBridge(bee, { url: 'ws://127.0.0.1:8765' });
179
+ bridge.connect();
180
+
181
+ // stesso parser, senza socket:
182
+ bridge.ingest({ type: 'metric', payload: { name: 'loss', value: 0.12 } });
183
+ ```
184
+
185
+ Forma del filo:
186
+
187
+ ```json
188
+ { "type": "log", "payload": { "message": "epoch 3" } }
189
+ { "type": "error", "message": "CUDA OOM" }
190
+ ```
191
+
192
+ Sender di esempio (solo stdlib, nessun pip):
193
+
194
+ ```bash
195
+ python examples/python/sender.py
196
+ ```
197
+
198
+ Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
199
+
200
+ ## npm / TypeScript
201
+
202
+ ```bash
203
+ npm install
204
+ npm run typecheck
205
+ ```
206
+
207
+ ```js
208
+ import { BeeLadybugCore } from 'bee-ladybug';
209
+ ```
210
+
211
+ Il runtime resta JavaScript ESM. I tipi stanno in `src/index.d.ts` (`package.json` → `"types"`).
212
+ Keyword e repository puntano a **BeeLadybug-universal**, non a BeeEngine.
213
+
214
+ ```bash
215
+ npm run typecheck
216
+ npm run lint
217
+ ```
218
+
219
+ Versione runtime e pacchetto: **0.2.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali). Su npm ora risulta `0.1.0`: il prossimo `npm publish` sarà `0.2.0`.
220
+
221
+ ## Assistente AI (gancio, nessun vendor)
222
+
223
+ BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta il proprio modello:
224
+
225
+ ```js
226
+ bee.setAssistant({
227
+ name: 'ollama',
228
+ async complete({ question, snapshot }) {
229
+ const res = await fetch('http://127.0.0.1:11434/api/generate', {
230
+ method: 'POST',
231
+ headers: { 'Content-Type': 'application/json' },
232
+ body: JSON.stringify({
233
+ model: 'llama3',
234
+ prompt: `${question}\n${JSON.stringify(snapshot)}`,
235
+ stream: false
236
+ })
237
+ });
238
+ const data = await res.json();
239
+ return data.response;
240
+ }
241
+ });
242
+
243
+ await bee.ask('perché il loss sale?');
244
+ ```
245
+
246
+ Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI**. Senza `setAssistant`, il bottone spiega come collegarlo.
247
+
248
+ ## API Core
249
+
250
+ ```bash
251
+ npm login
252
+ npm publish --access public
253
+ ```
254
+
168
255
  ## API Core
169
256
 
170
257
  ```js
@@ -190,14 +277,15 @@ bee.getHud();
190
277
  bee.show(); bee.hide(); bee.toggle();
191
278
  bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
192
279
  bee.applySlowMo(); bee.restoreRealtime();
280
+ bee.setAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
281
+ await bee.ask('why fps drop?');
193
282
  bee.destroy();
194
283
  ```
195
284
 
196
285
  ## Roadmap
197
286
 
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'`.
287
+ 1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI. Fatto.
288
+ 2. **npm 0.2.0** — `npm publish` (registry è ancora 0.1.0).
201
289
 
202
290
  ## Come contribuire
203
291
 
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.2.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.2.0';
12
13
 
13
14
  export const CORE_DEFAULTS = Object.freeze({
14
15
  toggleKey: 'F2',
@@ -92,6 +93,8 @@ 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.#assistant = null;
97
+ this.#askBusy = false;
95
98
 
96
99
  this.ui = null;
97
100
  if (cfg.ui !== false && typeof document !== 'undefined') {
@@ -121,6 +124,8 @@ export class BeeLadybugCore {
121
124
  #onKeyDown;
122
125
  #onTick;
123
126
  #started;
127
+ #assistant;
128
+ #askBusy;
124
129
 
125
130
  /**
126
131
  * Public adapter contract. `type` is a channel name; `payload` is raw data.
@@ -153,6 +158,87 @@ export class BeeLadybugCore {
153
158
  return () => this.#listeners.delete(handler);
154
159
  }
155
160
 
161
+ get hasAssistant() {
162
+ return Boolean(this.#assistant);
163
+ }
164
+
165
+ get assistantBusy() {
166
+ return this.#askBusy;
167
+ }
168
+
169
+ /**
170
+ * Plug in any model. BeeLadybug does not call OpenAI/Ollama itself.
171
+ * @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
172
+ */
173
+ setAssistant(provider) {
174
+ const next = normalizeAssistant(provider);
175
+ if (!next) {
176
+ this.sendData('warn', {
177
+ source: 'ai',
178
+ message: 'setAssistant() needs { complete(ctx) }. No vendor is bundled.'
179
+ });
180
+ return this;
181
+ }
182
+ this.#assistant = next;
183
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: next.name });
184
+ this.sendData('log', {
185
+ source: 'ai',
186
+ message: `assistant ready (${next.name})`
187
+ });
188
+ return this;
189
+ }
190
+
191
+ clearAssistant() {
192
+ this.#assistant = null;
193
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: 'off' });
194
+ return this;
195
+ }
196
+
197
+ /**
198
+ * Send recent telemetry to the registered assistant and print the answer.
199
+ * @param {string} [question]
200
+ * @returns {Promise<string|null>}
201
+ */
202
+ async ask(question = 'Diagnose the current telemetry.') {
203
+ if (!this.#assistant) {
204
+ this.sendData('warn', {
205
+ source: 'ai',
206
+ message: 'No assistant. bee.setAssistant({ name, complete }) then ASK AI.'
207
+ });
208
+ return null;
209
+ }
210
+ if (this.#askBusy) {
211
+ this.sendData('warn', { source: 'ai', message: 'assistant busy' });
212
+ return null;
213
+ }
214
+ const q = String(question || 'Diagnose the current telemetry.');
215
+ this.#askBusy = true;
216
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: 'thinking' });
217
+ this.sendData('log', { source: 'ai', message: `ask: ${q}` });
218
+ try {
219
+ const snapshot = buildAssistantSnapshot(this, q);
220
+ const raw = await this.#assistant.complete({ question: q, snapshot });
221
+ const text = String(raw ?? '').trim() || '(empty answer)';
222
+ this.sendData('log', { source: 'ai', message: text });
223
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: this.#assistant.name });
224
+ return text;
225
+ } catch (err) {
226
+ this.sendData('error', {
227
+ source: 'ai',
228
+ message: err instanceof Error ? err.message : String(err)
229
+ });
230
+ this.sendData('state', {
231
+ source: 'ai',
232
+ key: 'ai.assist',
233
+ value: this.#assistant?.name ?? 'error'
234
+ });
235
+ return null;
236
+ } finally {
237
+ this.#askBusy = false;
238
+ }
239
+ }
240
+
241
+
156
242
  start() {
157
243
  if (this.#started) return this;
158
244
  this.#started = true;
@@ -194,6 +280,8 @@ export class BeeLadybugCore {
194
280
  this.#history.clear();
195
281
  this.#hud.clear();
196
282
  this.#latest.clear();
283
+ this.#assistant = null;
284
+ this.#askBusy = false;
197
285
  this.ui?.destroy();
198
286
  this.ui = null;
199
287
  return this;
@@ -276,7 +364,8 @@ export class BeeLadybugCore {
276
364
  coreFps: this.coreFps,
277
365
  logCount: this.#logs.length,
278
366
  hud: this.getHud(),
279
- mode: this.#modeLabel()
367
+ mode: this.#modeLabel(),
368
+ assistant: this.#assistant ? this.#assistant.name : null
280
369
  };
281
370
  }
282
371
 
@@ -169,6 +169,7 @@ const STYLES = `
169
169
  .bl-foot {
170
170
  display: flex;
171
171
  align-items: center;
172
+ flex-wrap: wrap;
172
173
  gap: 6px;
173
174
  padding: 8px 10px;
174
175
  border-top: 1px solid rgba(255, 59, 78, 0.22);
@@ -205,7 +206,7 @@ export class UIOverlay {
205
206
  */
206
207
  constructor(core, options = {}) {
207
208
  this.core = core;
208
- this.#options = options;
209
+ void options;
209
210
  this.#host = null;
210
211
  this.#root = null;
211
212
  this.#els = null;
@@ -214,7 +215,6 @@ export class UIOverlay {
214
215
  this.#mounted = false;
215
216
  }
216
217
 
217
- #options;
218
218
  #host;
219
219
  #root;
220
220
  #els;
@@ -268,6 +268,7 @@ export class UIOverlay {
268
268
  <button class="bl-btn" data-act="slow" type="button">F3 SLOW</button>
269
269
  <button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
270
270
  <button class="bl-btn" data-act="live" type="button">1x LIVE</button>
271
+ <button class="bl-btn" data-act="ask" type="button">ASK AI</button>
271
272
  <span class="bl-hint">F2 overlay</span>
272
273
  </div>
273
274
  </div>
@@ -295,6 +296,7 @@ export class UIOverlay {
295
296
  if (act === 'slow') this.core.applySlowMo();
296
297
  else if (act === 'freeze') this.core.toggleFreeze();
297
298
  else if (act === 'live') this.core.restoreRealtime();
299
+ else if (act === 'ask') this.core.ask();
298
300
  });
299
301
 
300
302
  this.#els.console.addEventListener('scroll', () => {
@@ -415,6 +417,11 @@ export class UIOverlay {
415
417
  freeze.dataset.hot = state.frozen ? 'true' : 'false';
416
418
  slow.dataset.hot = !state.frozen && state.timeScale !== 1 ? 'true' : 'false';
417
419
  live.dataset.hot = !state.frozen && state.timeScale === 1 ? 'true' : 'false';
420
+ const ask = this.#root.querySelector('[data-act="ask"]');
421
+ if (ask) {
422
+ ask.dataset.hot = this.core.hasAssistant ? 'true' : 'false';
423
+ ask.disabled = Boolean(this.core.assistantBusy);
424
+ }
418
425
  }
419
426
 
420
427
  #paintGraph(series) {
package/src/index.d.ts ADDED
@@ -0,0 +1,272 @@
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
+ }
62
+
63
+ export interface CoreOptions {
64
+ toggleKey?: string;
65
+ slowKey?: string;
66
+ freezeKey?: string;
67
+ slowScale?: number;
68
+ maxLogs?: number;
69
+ historySize?: number;
70
+ mount?: 'auto' | false | HTMLElement;
71
+ ui?: boolean;
72
+ autoAttach?: boolean;
73
+ autoStart?: boolean;
74
+ overlay?: Record<string, unknown>;
75
+ }
76
+
77
+ export const CORE_DEFAULTS: Required<Omit<CoreOptions, 'overlay' | 'mount'>> & {
78
+ mount: 'auto';
79
+ };
80
+
81
+ export interface AssistantSnapshot {
82
+ question: string;
83
+ version: string;
84
+ state: CoreState;
85
+ hud: Record<string, HudEntry>;
86
+ logs: Array<{
87
+ type: string;
88
+ level: LogLevel | null;
89
+ source: string;
90
+ message: string;
91
+ frame: number;
92
+ }>;
93
+ history: {
94
+ name: string;
95
+ min: number;
96
+ max: number;
97
+ avg: number;
98
+ last: number;
99
+ samples: number[];
100
+ };
101
+ }
102
+
103
+ export interface AssistantProvider {
104
+ name?: string;
105
+ complete(input: { question: string; snapshot: AssistantSnapshot }): unknown;
106
+ }
107
+
108
+ export function formatSimTime(ms: number): string;
109
+
110
+ export class BeeLadybugCore {
111
+ readonly version: string;
112
+ toggleKey: string;
113
+ slowKey: string;
114
+ freezeKey: string;
115
+ slowScale: number;
116
+ maxLogs: number;
117
+ historySize: number;
118
+ visible: boolean;
119
+ frozen: boolean;
120
+ timeScale: number;
121
+ simTime: number;
122
+ frameCount: number;
123
+ coreFps: number;
124
+ ui: UIOverlay | null;
125
+
126
+ constructor(options?: CoreOptions);
127
+ sendData(type: string, payload?: unknown): BeePacket | null;
128
+ subscribe(handler: (packet: BeePacket) => void): () => void;
129
+ start(): this;
130
+ stop(): this;
131
+ attach(): this;
132
+ detach(): this;
133
+ destroy(): this;
134
+ show(): this;
135
+ hide(): this;
136
+ toggle(): this;
137
+ freeze(): this;
138
+ unfreeze(): this;
139
+ toggleFreeze(): this;
140
+ setTimeScale(scale: number): this;
141
+ applySlowMo(): this;
142
+ restoreRealtime(): this;
143
+ getState(): CoreState;
144
+ getLogs(): BeePacket[];
145
+ getHistory(name: string): MetricHistory;
146
+ getPrimaryHistory(): MetricHistory;
147
+ getHud(): Record<string, HudEntry>;
148
+ getLatest(type: string): BeePacket | null;
149
+ readonly hasAssistant: boolean;
150
+ readonly assistantBusy: boolean;
151
+ setAssistant(provider: AssistantProvider): this;
152
+ clearAssistant(): this;
153
+ ask(question?: string): Promise<string | null>;
154
+ }
155
+
156
+ export class UIOverlay {
157
+ core: BeeLadybugCore | null;
158
+ constructor(core: BeeLadybugCore, options?: Record<string, unknown>);
159
+ mount(target?: 'auto' | HTMLElement): this;
160
+ unmount(): this;
161
+ destroy(): this;
162
+ setVisible(visible: boolean): this;
163
+ onPacket(packet: BeePacket): void;
164
+ onClear(channel?: string): void;
165
+ render(): void;
166
+ }
167
+
168
+ export interface CanvasEntity {
169
+ name?: string;
170
+ label?: string;
171
+ id?: string | number;
172
+ role?: string;
173
+ primary?: boolean;
174
+ x?: number;
175
+ y?: number;
176
+ worldX?: number;
177
+ worldY?: number;
178
+ width?: number;
179
+ height?: number;
180
+ w?: number;
181
+ h?: number;
182
+ active?: boolean;
183
+ colliding?: boolean;
184
+ [key: string]: unknown;
185
+ }
186
+
187
+ export interface CanvasAdapterOptions {
188
+ canvas?: HTMLCanvasElement | null;
189
+ entities?: CanvasEntity[];
190
+ overlay?: boolean;
191
+ computeCollisions?: boolean;
192
+ colorActive?: string;
193
+ colorColliding?: string;
194
+ colorInactive?: string;
195
+ source?: string;
196
+ }
197
+
198
+ export const CANVAS_ADAPTER_DEFAULTS: Required<
199
+ Omit<CanvasAdapterOptions, 'canvas' | 'entities'>
200
+ >;
201
+
202
+ export class CanvasAdapter {
203
+ core: BeeLadybugCore | null;
204
+ canvas: HTMLCanvasElement | null;
205
+ entities: CanvasEntity[];
206
+ overlayEnabled: boolean;
207
+ computeCollisions: boolean;
208
+ frozen: boolean;
209
+ timeScale: number;
210
+ constructor(core: BeeLadybugCore, options?: CanvasAdapterOptions);
211
+ attach(): this;
212
+ detach(): this;
213
+ setCanvas(canvas: HTMLCanvasElement | null): this;
214
+ setEntities(entities: CanvasEntity[]): this;
215
+ pump(ctx?: CanvasRenderingContext2D): this;
216
+ }
217
+
218
+ export interface WebDOMAdapterOptions {
219
+ root?: ParentNode | null;
220
+ overlay?: boolean;
221
+ autoTick?: boolean;
222
+ watch?: string[];
223
+ source?: string;
224
+ colorHover?: string;
225
+ colorWatch?: string;
226
+ colorMargin?: string;
227
+ colorPadding?: string;
228
+ colorContent?: string;
229
+ }
230
+
231
+ export const WEB_DOM_ADAPTER_DEFAULTS: Required<Omit<WebDOMAdapterOptions, 'root'>>;
232
+
233
+ export class WebDOMAdapter {
234
+ core: BeeLadybugCore | null;
235
+ root: ParentNode | null;
236
+ overlayEnabled: boolean;
237
+ autoTick: boolean;
238
+ watch: string[];
239
+ frozen: boolean;
240
+ timeScale: number;
241
+ hover: Element | null;
242
+ constructor(core: BeeLadybugCore, options?: WebDOMAdapterOptions);
243
+ attach(): this;
244
+ detach(): this;
245
+ setRoot(root: ParentNode | null): this;
246
+ pump(): this;
247
+ }
248
+
249
+ export type PythonBridgeStatus = 'idle' | 'connecting' | 'open' | 'closed';
250
+
251
+ export interface PythonBridgeOptions {
252
+ url?: string;
253
+ autoReconnect?: boolean;
254
+ reconnectMs?: number;
255
+ source?: string;
256
+ }
257
+
258
+ export const PYTHON_BRIDGE_DEFAULTS: Required<PythonBridgeOptions>;
259
+
260
+ export class PythonBridge {
261
+ core: BeeLadybugCore | null;
262
+ url: string;
263
+ autoReconnect: boolean;
264
+ reconnectMs: number;
265
+ source: string;
266
+ status: PythonBridgeStatus;
267
+ readonly connected: boolean;
268
+ constructor(core: BeeLadybugCore, options?: PythonBridgeOptions);
269
+ connect(url?: string): this;
270
+ disconnect(): this;
271
+ ingest(raw: unknown): this;
272
+ }
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';