bee-ladybug 0.3.0 → 0.4.1

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/README.MD CHANGED
@@ -1,7 +1,9 @@
1
1
 
2
2
  # BeeLadybug
3
3
 
4
-
4
+ <p align="center">
5
+ <img src="https://raw.githubusercontent.com/antonioprosperi2-svg/BeeLadybug-universal/main/docs/logo.jpg" alt="BeeLadybug" width="720">
6
+ </p>
5
7
 
6
8
  Universal telemetry and visual inspection overlay.
7
9
 
@@ -37,6 +39,8 @@ bee-ladybug/
37
39
  │ └── adapters/
38
40
  │ ├── CanvasAdapter.js
39
41
  │ ├── WebDOMAdapter.js
42
+ │ ├── AntAdapter.js
43
+ │ ├── SpiderAdapter.js
40
44
  │ └── python/PythonBridge.js
41
45
  ├── examples/
42
46
  │ ├── core.html
@@ -96,6 +100,7 @@ Gli adapter parlano **solo** così. Uno scalare viene avvolto in `{ value }`.
96
100
  | `log` | `{ message, level?, source? }` | console |
97
101
  | `warn` | `{ message, source? }` | console ambra |
98
102
  | `error` | `{ message, source? }` | console rossa |
103
+ | `telemetry` | `{ kind?, duration?, source? }` | history + lampeggio pulsante adapter |
99
104
  | `clear` | `{ channel?: 'logs'\|'history'\|'hud' }` | reset buffer |
100
105
  | `sys` | emesso dal Core | freeze / visibilità / scala |
101
106
 
@@ -116,7 +121,7 @@ bee.subscribe((packet) => {
116
121
  });
117
122
  ```
118
123
 
119
- ![Overlay Core: la ⓘ accanto a LIVE spiega che il freeze ferma solo il clock fittizio](docs/overlay-core.jpg)
124
+ ![Overlay Core: la ⓘ accanto a LIVE spiega che il freeze ferma solo il clock fittizio](https://raw.githubusercontent.com/antonioprosperi2-svg/BeeLadybug-universal/main/docs/overlay-core.jpg)
120
125
 
121
126
  ## Tasti
122
127
 
@@ -172,6 +177,50 @@ adapter.attach();
172
177
 
173
178
  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.
174
179
 
180
+ ## Registro adapter (overlay)
181
+
182
+ Il Core non conosce ANT/SPIDER. Gli adapter si iscrivono con hook propri; l'overlay disegna un toggle per ciascuno.
183
+
184
+ ```js
185
+ bee.registerAdapter('probe', {
186
+ label: 'PROBE',
187
+ enable: () => observer.observe(root, opts),
188
+ disable: () => observer.disconnect()
189
+ });
190
+ adapter.detach(); // chiama bee.unregisterAdapter('probe')
191
+ ```
192
+
193
+ Click sul pulsante → `enable()` / `disable()`. Un pacchetto `warn` o `telemetry` con `source` uguale al nome registrato fa lampeggiare quel pulsante, al massimo una volta ogni 1.5s.
194
+
195
+ ## AntAdapter
196
+
197
+ ```js
198
+ import { BeeLadybugCore, AntAdapter } from './src/index.js';
199
+
200
+ const bee = new BeeLadybugCore();
201
+ const ant = new AntAdapter(bee, {
202
+ root: document.body,
203
+ threshold: 30 // mutazioni / secondo per nodo
204
+ });
205
+ ant.attach();
206
+ ```
207
+
208
+ `MutationObserver` indipendente da `WebDOMAdapter` (possono condividere lo stesso `root`). Se un nodo supera la soglia, arriva un `warn` `source: 'ant'`.
209
+
210
+ ## SpiderAdapter
211
+
212
+ ```js
213
+ import { BeeLadybugCore, SpiderAdapter } from './src/index.js';
214
+
215
+ const bee = new BeeLadybugCore();
216
+ const spider = new SpiderAdapter(bee, {
217
+ resourceThreshold: 500 // ms
218
+ });
219
+ spider.attach();
220
+ ```
221
+
222
+ `PerformanceObserver`: `longtask` > 50ms → `telemetry`; risorse > soglia → `warn`. Se il browser non espone `longtask` (Firefox/Safari), un log una tantum e quella parte si spegne senza errori.
223
+
175
224
  ## PythonBridge
176
225
 
177
226
  Python non vede l'overlay. Manda JSON. Il browser lo srotola in `sendData()`.
@@ -202,7 +251,7 @@ python examples/python/sender.py
202
251
 
203
252
  Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
204
253
 
205
- ![Overlay PythonBridge: ASK AI e quadratini mock/echo per scegliere il provider](docs/overlay-python.jpg)
254
+ ![Overlay PythonBridge: ASK AI e quadratini mock/echo per scegliere il provider](https://raw.githubusercontent.com/antonioprosperi2-svg/BeeLadybug-universal/main/docs/overlay-python.jpg)
206
255
 
207
256
  ## npm / TypeScript
208
257
 
@@ -228,7 +277,7 @@ npm login
228
277
  npm publish --access public
229
278
  ```
230
279
 
231
- Versione runtime e pacchetto: **0.3.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
280
+ Versione runtime e pacchetto: **0.4.1** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
232
281
 
233
282
  ## Assistente AI (gancio, nessun vendor)
234
283
 
@@ -293,13 +342,15 @@ bee.applySlowMo(); bee.restoreRealtime();
293
342
  bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
294
343
  bee.setActiveAssistant('mock');
295
344
  await bee.ask('why fps drop?');
345
+ bee.registerAdapter('probe', { label: 'PROBE', enable() {}, disable() {} });
346
+ bee.toggleAdapter('probe');
296
347
  bee.destroy();
297
348
  ```
298
349
 
299
350
  ## Roadmap
300
351
 
301
- 1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
302
- 2. **npm 0.3.0** — `npm publish`.
352
+ 1. **Ora** — Core, adapter, registro toggle overlay, Ant/Spider, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
353
+ 2. **npm 0.4.1** — `npm publish`.
303
354
 
304
355
  ## Come contribuire
305
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
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",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "typecheck": "tsc --noEmit",
21
21
  "lint": "eslint src examples",
22
- "test": "node --test tests/**/*.test.js"
22
+ "test": "node --test tests/*.test.js"
23
23
  },
24
24
  "keywords": [
25
25
  "debug",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "homepage": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal#readme",
47
47
  "engines": {
48
- "node": ">=18"
48
+ "node": ">=20"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.36.0",
@@ -0,0 +1,144 @@
1
+ /**
2
+ * AntAdapter — DOM mutation rate tracker.
3
+ *
4
+ * Independent MutationObserver. Can sit on the same root as WebDOMAdapter.
5
+ * The core must never import this file.
6
+ *
7
+ * @example
8
+ * const ant = new AntAdapter(core, { root: document.body, threshold: 30 });
9
+ * ant.attach();
10
+ */
11
+
12
+ export const ANT_ADAPTER_DEFAULTS = Object.freeze({
13
+ threshold: 30,
14
+ windowMs: 1000,
15
+ source: 'ant'
16
+ });
17
+
18
+ export class AntAdapter {
19
+ /**
20
+ * @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
21
+ * @param {Partial<typeof ANT_ADAPTER_DEFAULTS> & {
22
+ * root?: ParentNode | null
23
+ * }} [options]
24
+ */
25
+ constructor(core, options = {}) {
26
+ const cfg = { ...ANT_ADAPTER_DEFAULTS, ...options };
27
+ this.core = core;
28
+ this.root = options.root !== undefined
29
+ ? options.root
30
+ : (typeof document !== 'undefined' ? document.body : null);
31
+ this.threshold = positive(cfg.threshold, ANT_ADAPTER_DEFAULTS.threshold);
32
+ this.windowMs = positive(cfg.windowMs, ANT_ADAPTER_DEFAULTS.windowMs);
33
+ this.source = cfg.source;
34
+
35
+ this.#attached = false;
36
+ this.#observer = null;
37
+ this.#counts = new WeakMap();
38
+ this.#onMutate = (records) => this.#ingest(records);
39
+ }
40
+
41
+ #attached;
42
+ #observer;
43
+ #counts;
44
+ #onMutate;
45
+
46
+ attach() {
47
+ if (this.#attached) return this;
48
+ this.#attached = true;
49
+ this.#enable();
50
+ this.core?.registerAdapter('ant', {
51
+ enable: () => this.#enable(),
52
+ disable: () => this.#disable(),
53
+ label: 'ANT'
54
+ });
55
+ this.core?.sendData('log', {
56
+ source: this.source,
57
+ message: 'AntAdapter attached. Watching DOM mutation rate.'
58
+ });
59
+ return this;
60
+ }
61
+
62
+ detach() {
63
+ if (!this.#attached) return this;
64
+ this.#attached = false;
65
+ this.#disable();
66
+ this.core?.unregisterAdapter('ant');
67
+ return this;
68
+ }
69
+
70
+ #enable() {
71
+ if (!this.#attached || this.#observer) return;
72
+ if (typeof MutationObserver === 'undefined' || !this.root) return;
73
+ this.#counts = new WeakMap();
74
+ this.#observer = new MutationObserver(this.#onMutate);
75
+ try {
76
+ this.#observer.observe(this.root, {
77
+ childList: true,
78
+ subtree: true,
79
+ attributes: true,
80
+ characterData: true
81
+ });
82
+ } catch {
83
+ this.#observer.disconnect();
84
+ this.#observer = null;
85
+ }
86
+ }
87
+
88
+ #disable() {
89
+ if (this.#observer) this.#observer.disconnect();
90
+ this.#observer = null;
91
+ this.#counts = new WeakMap();
92
+ }
93
+
94
+ #ingest(records) {
95
+ if (!this.#attached || !this.core || !records?.length) return;
96
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
97
+ for (let i = 0; i < records.length; i++) {
98
+ const node = records[i]?.target;
99
+ if (!node || isBeeChrome(node)) continue;
100
+
101
+ let slot = this.#counts.get(node);
102
+ if (!slot || now - slot.windowStart >= this.windowMs) {
103
+ slot = { count: 0, windowStart: now, warned: false };
104
+ this.#counts.set(node, slot);
105
+ }
106
+ slot.count += 1;
107
+ if (slot.count < this.threshold || slot.warned) continue;
108
+
109
+ slot.warned = true;
110
+ const selector = nodeSelector(node);
111
+ this.core.sendData('warn', {
112
+ source: this.source,
113
+ message: `High Mutation Rate on ${selector}`,
114
+ node: selector
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ function positive(value, fallback) {
121
+ const n = Number(value);
122
+ return Number.isFinite(n) && n > 0 ? n : fallback;
123
+ }
124
+
125
+ function nodeSelector(node) {
126
+ const el = node && node.nodeType === 1 ? node : node?.parentElement;
127
+ if (!el || !el.tagName) return 'node';
128
+ if (el.id) return `#${el.id}`;
129
+ const cls = el.classList && el.classList[0];
130
+ if (cls) return `.${cls}`;
131
+ return el.tagName.toLowerCase();
132
+ }
133
+
134
+ function isBeeChrome(node) {
135
+ let el = node && node.nodeType === 1 ? node : node?.parentElement;
136
+ while (el) {
137
+ const tag = el.tagName;
138
+ if (tag === 'BEE-LADYBUG-OVERLAY') return true;
139
+ if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
140
+ if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
141
+ el = el.parentElement;
142
+ }
143
+ return false;
144
+ }
@@ -15,11 +15,17 @@
15
15
  * adapter.attach();
16
16
  * // after updating entity positions each host frame:
17
17
  * adapter.pump();
18
+ *
19
+ * @example
20
+ * // third-party / bookmarklet: no access to the host loop
21
+ * const remote = new CanvasAdapter(core, { canvas, entities, autoPump: true });
22
+ * remote.attach();
18
23
  */
19
24
 
20
25
  export const CANVAS_ADAPTER_DEFAULTS = Object.freeze({
21
26
  overlay: true,
22
27
  computeCollisions: true,
28
+ autoPump: false,
23
29
  colorActive: '#3dff6a',
24
30
  colorColliding: '#ff3b3b',
25
31
  colorInactive: '#8a8a8a',
@@ -41,6 +47,7 @@ export class CanvasAdapter {
41
47
  this.entities = options.entities ?? [];
42
48
  this.overlayEnabled = cfg.overlay;
43
49
  this.computeCollisions = cfg.computeCollisions;
50
+ this.autoPump = Boolean(cfg.autoPump);
44
51
  this.colorActive = cfg.colorActive;
45
52
  this.colorColliding = cfg.colorColliding;
46
53
  this.colorInactive = cfg.colorInactive;
@@ -54,19 +61,35 @@ export class CanvasAdapter {
54
61
  this.#attached = false;
55
62
  this.#overlay = null;
56
63
  this.#unsubscribe = null;
64
+ this.#rafId = 0;
65
+ this.#pumpFromAuto = false;
66
+ this.#manualPumpWarned = false;
57
67
  this.#lastPump = 0;
58
68
  this.#fps = 0;
59
69
  this.#wasColliding = new Set();
60
70
  this.#onResize = () => this.#syncOverlay();
71
+ this.#onTick = () => {
72
+ this.#rafId = requestAnimationFrame(this.#onTick);
73
+ this.#pumpFromAuto = true;
74
+ try {
75
+ this.pump();
76
+ } finally {
77
+ this.#pumpFromAuto = false;
78
+ }
79
+ };
61
80
  }
62
81
 
63
82
  #attached;
64
83
  #overlay;
65
84
  #unsubscribe;
85
+ #rafId;
86
+ #pumpFromAuto;
87
+ #manualPumpWarned;
66
88
  #lastPump;
67
89
  #fps;
68
90
  #wasColliding;
69
91
  #onResize;
92
+ #onTick;
70
93
 
71
94
  attach() {
72
95
  if (this.#attached) return this;
@@ -83,6 +106,7 @@ export class CanvasAdapter {
83
106
  window.addEventListener('resize', this.#onResize);
84
107
  window.addEventListener('scroll', this.#onResize, true);
85
108
  }
109
+ if (this.autoPump) this.startAutoPump();
86
110
 
87
111
  this.core?.sendData('log', {
88
112
  source: this.source,
@@ -99,6 +123,7 @@ export class CanvasAdapter {
99
123
  detach() {
100
124
  if (!this.#attached) return this;
101
125
  this.#attached = false;
126
+ this.#stopAutoPumpLoop();
102
127
  if (this.#unsubscribe) this.#unsubscribe();
103
128
  this.#unsubscribe = null;
104
129
  this.#unmountOverlay();
@@ -109,6 +134,20 @@ export class CanvasAdapter {
109
134
  return this;
110
135
  }
111
136
 
137
+ /**
138
+ * Drive `pump()` from an internal `requestAnimationFrame` loop.
139
+ * Use this when the host game loop cannot call `pump()` (bookmarklet / console).
140
+ */
141
+ startAutoPump() {
142
+ this.autoPump = true;
143
+ if (!this.#attached) return this;
144
+ if (this.#rafId) return this;
145
+ if (typeof requestAnimationFrame !== 'function') return this;
146
+ this.#manualPumpWarned = false;
147
+ this.#rafId = requestAnimationFrame(this.#onTick);
148
+ return this;
149
+ }
150
+
112
151
  /** @param {HTMLCanvasElement | null} canvas */
113
152
  setCanvas(canvas) {
114
153
  this.canvas = canvas;
@@ -126,10 +165,15 @@ export class CanvasAdapter {
126
165
  * Call once per host frame, after entity positions are updated.
127
166
  * Pass a 2D context only if overlay is disabled and you want hitboxes
128
167
  * drawn onto the game canvas itself.
168
+ * Ignored (with a warning) while `autoPump` owns the frame loop.
129
169
  * @param {CanvasRenderingContext2D} [ctx]
130
170
  */
131
171
  pump(ctx) {
132
172
  if (!this.#attached || !this.core) return this;
173
+ if (this.#rafId && !this.#pumpFromAuto) {
174
+ this.#warnManualPump();
175
+ return this;
176
+ }
133
177
 
134
178
  const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
135
179
  if (this.#lastPump) {
@@ -147,6 +191,25 @@ export class CanvasAdapter {
147
191
  return this;
148
192
  }
149
193
 
194
+ #stopAutoPumpLoop() {
195
+ if (this.#rafId && typeof cancelAnimationFrame === 'function') {
196
+ cancelAnimationFrame(this.#rafId);
197
+ }
198
+ this.#rafId = 0;
199
+ this.#pumpFromAuto = false;
200
+ this.#manualPumpWarned = false;
201
+ }
202
+
203
+ #warnManualPump() {
204
+ if (this.#manualPumpWarned) return;
205
+ this.#manualPumpWarned = true;
206
+ const message = 'pump() skipped: autoPump already drives the frame loop.';
207
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
208
+ console.warn(`[BeeLadybug] CanvasAdapter ${message}`);
209
+ }
210
+ this.core.sendData('warn', { source: this.source, message });
211
+ }
212
+
150
213
  #onCore(packet) {
151
214
  if (packet.type !== 'sys') return;
152
215
  const op = packet.payload.op;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * SpiderAdapter — long-task and slow-resource tracker.
3
+ *
4
+ * Native PerformanceObserver only. The core must never import this file.
5
+ *
6
+ * @example
7
+ * const spider = new SpiderAdapter(core, { resourceThreshold: 500 });
8
+ * spider.attach();
9
+ */
10
+
11
+ export const SPIDER_ADAPTER_DEFAULTS = Object.freeze({
12
+ longTaskThreshold: 50,
13
+ resourceThreshold: 500,
14
+ source: 'spider'
15
+ });
16
+
17
+ export class SpiderAdapter {
18
+ /**
19
+ * @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
20
+ * @param {Partial<typeof SPIDER_ADAPTER_DEFAULTS>} [options]
21
+ */
22
+ constructor(core, options = {}) {
23
+ const cfg = { ...SPIDER_ADAPTER_DEFAULTS, ...options };
24
+ this.core = core;
25
+ this.longTaskThreshold = positive(cfg.longTaskThreshold, SPIDER_ADAPTER_DEFAULTS.longTaskThreshold);
26
+ this.resourceThreshold = positive(cfg.resourceThreshold, SPIDER_ADAPTER_DEFAULTS.resourceThreshold);
27
+ this.source = cfg.source;
28
+
29
+ this.#attached = false;
30
+ this.#longObserver = null;
31
+ this.#resourceObserver = null;
32
+ this.#longTaskMissingLogged = false;
33
+ this.#onLongTask = (list) => this.#ingestLongTasks(list);
34
+ this.#onResource = (list) => this.#ingestResources(list);
35
+ }
36
+
37
+ #attached;
38
+ #longObserver;
39
+ #resourceObserver;
40
+ #longTaskMissingLogged;
41
+ #onLongTask;
42
+ #onResource;
43
+
44
+ attach() {
45
+ if (this.#attached) return this;
46
+ this.#attached = true;
47
+ this.#enable();
48
+ this.core?.registerAdapter('spider', {
49
+ enable: () => this.#enable(),
50
+ disable: () => this.#disable(),
51
+ label: 'SPIDER'
52
+ });
53
+ this.core?.sendData('log', {
54
+ source: this.source,
55
+ message: 'SpiderAdapter attached. Watching long tasks and slow resources.'
56
+ });
57
+ return this;
58
+ }
59
+
60
+ detach() {
61
+ if (!this.#attached) return this;
62
+ this.#attached = false;
63
+ this.#disable();
64
+ this.core?.unregisterAdapter('spider');
65
+ return this;
66
+ }
67
+
68
+ #enable() {
69
+ if (!this.#attached) return;
70
+ if (typeof PerformanceObserver !== 'function') {
71
+ this.#logLongTaskUnavailable();
72
+ return;
73
+ }
74
+ const types = supportedEntryTypes();
75
+ if (types.includes('longtask')) this.#watchLongTasks();
76
+ else this.#logLongTaskUnavailable();
77
+ if (types.includes('resource')) this.#watchResources();
78
+ }
79
+
80
+ #disable() {
81
+ if (this.#longObserver) this.#longObserver.disconnect();
82
+ if (this.#resourceObserver) this.#resourceObserver.disconnect();
83
+ this.#longObserver = null;
84
+ this.#resourceObserver = null;
85
+ }
86
+
87
+ #watchLongTasks() {
88
+ if (this.#longObserver) return;
89
+ try {
90
+ this.#longObserver = new PerformanceObserver(this.#onLongTask);
91
+ this.#longObserver.observe({ entryTypes: ['longtask'] });
92
+ } catch {
93
+ this.#longObserver = null;
94
+ this.#logLongTaskUnavailable();
95
+ }
96
+ }
97
+
98
+ #watchResources() {
99
+ if (this.#resourceObserver) return;
100
+ try {
101
+ this.#resourceObserver = new PerformanceObserver(this.#onResource);
102
+ this.#resourceObserver.observe({ entryTypes: ['resource'] });
103
+ } catch {
104
+ this.#resourceObserver = null;
105
+ }
106
+ }
107
+
108
+ #ingestLongTasks(list) {
109
+ if (!this.#attached || !this.core) return;
110
+ const entries = list?.getEntries?.() ?? [];
111
+ for (let i = 0; i < entries.length; i++) {
112
+ const entry = entries[i];
113
+ if (!entry || entry.duration <= this.longTaskThreshold) continue;
114
+ this.core.sendData('telemetry', {
115
+ source: this.source,
116
+ kind: 'longtask',
117
+ duration: entry.duration,
118
+ attribution: entry.attribution?.[0]?.containerType ?? 'unknown'
119
+ });
120
+ }
121
+ }
122
+
123
+ #ingestResources(list) {
124
+ if (!this.#attached || !this.core) return;
125
+ const entries = list?.getEntries?.() ?? [];
126
+ for (let i = 0; i < entries.length; i++) {
127
+ const entry = entries[i];
128
+ if (!entry || entry.duration <= this.resourceThreshold) continue;
129
+ this.core.sendData('warn', {
130
+ source: this.source,
131
+ message: `Risorsa lenta: ${entry.name} (${Math.round(entry.duration)}ms)`
132
+ });
133
+ }
134
+ }
135
+
136
+ #logLongTaskUnavailable() {
137
+ if (this.#longTaskMissingLogged) return;
138
+ this.#longTaskMissingLogged = true;
139
+ this.core?.sendData('log', {
140
+ source: this.source,
141
+ message: 'Long task detection non disponibile in questo browser'
142
+ });
143
+ }
144
+ }
145
+
146
+ function positive(value, fallback) {
147
+ const n = Number(value);
148
+ return Number.isFinite(n) && n > 0 ? n : fallback;
149
+ }
150
+
151
+ function supportedEntryTypes() {
152
+ if (typeof PerformanceObserver !== 'function') return [];
153
+ const list = PerformanceObserver.supportedEntryTypes;
154
+ if (!list) return [];
155
+ try {
156
+ return Array.from(list);
157
+ } catch {
158
+ return [];
159
+ }
160
+ }
@@ -9,7 +9,7 @@
9
9
  import { UIOverlay } from './UIOverlay.js';
10
10
  import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
11
11
 
12
- export const BEE_LADYBUG_VERSION = '0.3.0';
12
+ export const BEE_LADYBUG_VERSION = '0.4.1';
13
13
 
14
14
  export const CORE_DEFAULTS = Object.freeze({
15
15
  toggleKey: 'F2',
@@ -35,7 +35,8 @@ export const WELL_KNOWN_TYPES = Object.freeze([
35
35
  'error',
36
36
  'state',
37
37
  'sys',
38
- 'clear'
38
+ 'clear',
39
+ 'telemetry'
39
40
  ]);
40
41
 
41
42
  /**
@@ -96,6 +97,7 @@ export class BeeLadybugCore {
96
97
  this.#assistants = new Map();
97
98
  this.#activeAssistant = null;
98
99
  this.#askBusy = false;
100
+ this.#adapters = new Map();
99
101
 
100
102
  this.ui = null;
101
103
  if (cfg.ui !== false && typeof document !== 'undefined') {
@@ -130,6 +132,8 @@ export class BeeLadybugCore {
130
132
  /** @type {string | null} */
131
133
  #activeAssistant;
132
134
  #askBusy;
135
+ /** @type {Map<string, { name: string, label: string, enabled: boolean, enable: Function, disable: Function }>} */
136
+ #adapters;
133
137
 
134
138
  /**
135
139
  * Public adapter contract. `type` is a channel name; `payload` is raw data.
@@ -281,6 +285,86 @@ export class BeeLadybugCore {
281
285
  return this.#activeAssistant ? this.#assistants.get(this.#activeAssistant) ?? null : null;
282
286
  }
283
287
 
288
+ /**
289
+ * Register a toggleable diagnostic adapter. The core stores hooks only —
290
+ * it does not import or special-case adapter modules.
291
+ * @param {string} name
292
+ * @param {{ enable: () => void, disable: () => void, label?: string }} hooks
293
+ */
294
+ registerAdapter(name, hooks = {}) {
295
+ const key = String(name || '').trim();
296
+ if (!key || typeof hooks.enable !== 'function' || typeof hooks.disable !== 'function') {
297
+ this.sendData('warn', {
298
+ source: 'core',
299
+ message: 'registerAdapter() needs a name and { enable, disable }.'
300
+ });
301
+ return this;
302
+ }
303
+ const label = String(hooks.label || key).trim() || key;
304
+ this.#adapters.set(key, {
305
+ name: key,
306
+ label,
307
+ enabled: true,
308
+ enable: hooks.enable,
309
+ disable: hooks.disable
310
+ });
311
+ this.sendData('sys', { op: 'adapter', name: key, registered: true, label, enabled: true });
312
+ return this;
313
+ }
314
+
315
+ /**
316
+ * @param {string} name
317
+ */
318
+ unregisterAdapter(name) {
319
+ const key = String(name || '').trim();
320
+ if (!key || !this.#adapters.has(key)) return this;
321
+ this.#adapters.delete(key);
322
+ this.sendData('sys', { op: 'adapter', name: key, registered: false });
323
+ return this;
324
+ }
325
+
326
+ /** Registered diagnostic adapters, insertion order. */
327
+ getAdapters() {
328
+ return [...this.#adapters.values()].map((entry) => ({
329
+ name: entry.name,
330
+ label: entry.label,
331
+ enabled: entry.enabled
332
+ }));
333
+ }
334
+
335
+ /**
336
+ * @param {string} name
337
+ */
338
+ toggleAdapter(name) {
339
+ const entry = this.#adapters.get(String(name || '').trim());
340
+ if (!entry) return this;
341
+ return this.setAdapterEnabled(entry.name, !entry.enabled);
342
+ }
343
+
344
+ /**
345
+ * @param {string} name
346
+ * @param {boolean} enabled
347
+ */
348
+ setAdapterEnabled(name, enabled) {
349
+ const entry = this.#adapters.get(String(name || '').trim());
350
+ if (!entry) return this;
351
+ const next = Boolean(enabled);
352
+ if (entry.enabled === next) return this;
353
+ try {
354
+ if (next) entry.enable();
355
+ else entry.disable();
356
+ entry.enabled = next;
357
+ } catch (err) {
358
+ console.warn('[BeeLadybug] adapter toggle error', err);
359
+ }
360
+ this.sendData('sys', {
361
+ op: 'adapter',
362
+ name: entry.name,
363
+ enabled: entry.enabled
364
+ });
365
+ return this;
366
+ }
367
+
284
368
  /**
285
369
  * @param {{ name?: string, complete: Function }} provider
286
370
  * @param {{ activate: boolean, via: string }} opts
@@ -353,6 +437,7 @@ export class BeeLadybugCore {
353
437
  this.#assistants.clear();
354
438
  this.#activeAssistant = null;
355
439
  this.#askBusy = false;
440
+ this.#adapters.clear();
356
441
  this.ui?.destroy();
357
442
  this.ui = null;
358
443
  return this;
@@ -441,7 +526,8 @@ export class BeeLadybugCore {
441
526
  hud: this.getHud(),
442
527
  mode: this.#modeLabel(),
443
528
  assistant: this.#activeAssistant,
444
- assistants: this.getAssistants()
529
+ assistants: this.getAssistants(),
530
+ adapters: this.getAdapters()
445
531
  };
446
532
  }
447
533
 
@@ -553,6 +639,13 @@ export class BeeLadybugCore {
553
639
  return;
554
640
  }
555
641
 
642
+ if (type === 'telemetry') {
643
+ const name = String(payload.kind ?? payload.name ?? 'telemetry');
644
+ const value = numeric(payload.duration ?? payload.value);
645
+ if (value != null) this.#pushHistory(name, value);
646
+ return;
647
+ }
648
+
556
649
  if (isConsoleType(type) || packet.level) {
557
650
  this.#pushLog(packet);
558
651
  }
@@ -247,6 +247,41 @@ const STYLES = `
247
247
  .bl-providers:empty {
248
248
  display: none;
249
249
  }
250
+ .bl-adapters {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ gap: 3px;
254
+ }
255
+ .bl-adapters:empty {
256
+ display: none;
257
+ }
258
+ .bl-adapter {
259
+ appearance: none;
260
+ background: #16181f;
261
+ color: #8b91a3;
262
+ border: 1px solid rgba(139, 145, 163, 0.45);
263
+ font: 700 9px ui-monospace, Consolas, monospace;
264
+ letter-spacing: 0.06em;
265
+ padding: 5px 7px;
266
+ cursor: pointer;
267
+ }
268
+ .bl-adapter[data-hot="true"] {
269
+ color: #fff6d8;
270
+ border-color: #ffd36a;
271
+ }
272
+ .bl-adapter[data-flash="true"] {
273
+ animation: bl-adapter-flash 0.55s ease;
274
+ }
275
+ @keyframes bl-adapter-flash {
276
+ 0% {
277
+ background: #ffd36a;
278
+ color: #05060a;
279
+ border-color: #ffd36a;
280
+ }
281
+ 100% {
282
+ background: #16181f;
283
+ }
284
+ }
250
285
  .bl-provider {
251
286
  appearance: none;
252
287
  width: 22px;
@@ -271,6 +306,12 @@ const STYLES = `
271
306
  }
272
307
  @media (prefers-reduced-motion: reduce) {
273
308
  .bl-scan { display: none; }
309
+ .bl-adapter[data-flash="true"] {
310
+ animation: none;
311
+ background: #ffd36a;
312
+ color: #05060a;
313
+ border-color: #ffd36a;
314
+ }
274
315
  }
275
316
  `;
276
317
 
@@ -289,6 +330,9 @@ export class UIOverlay {
289
330
  this.#lastLogId = 0;
290
331
  this.#mounted = false;
291
332
  this.#providerSig = '';
333
+ this.#adapterSig = '';
334
+ this.#flashAt = new Map();
335
+ this.#flashTimers = new Map();
292
336
  }
293
337
 
294
338
  #host;
@@ -298,6 +342,9 @@ export class UIOverlay {
298
342
  #lastLogId;
299
343
  #mounted;
300
344
  #providerSig;
345
+ #adapterSig;
346
+ #flashAt;
347
+ #flashTimers;
301
348
 
302
349
  /** @param {'auto' | HTMLElement} [target] */
303
350
  mount(target = 'auto') {
@@ -353,6 +400,7 @@ export class UIOverlay {
353
400
  <button class="bl-btn" data-act="live" type="button">1x LIVE</button>
354
401
  <button class="bl-btn" data-act="ask" type="button">ASK AI</button>
355
402
  <span class="bl-providers" data-el="providers"></span>
403
+ <span class="bl-adapters" data-el="adapters"></span>
356
404
  <span class="bl-hint">F2 overlay</span>
357
405
  </div>
358
406
  </div>
@@ -371,7 +419,8 @@ export class UIOverlay {
371
419
  graphStats: this.#root.querySelector('[data-el="graphStats"]'),
372
420
  hud: this.#root.querySelector('[data-el="hud"]'),
373
421
  console: this.#root.querySelector('[data-el="console"]'),
374
- providers: this.#root.querySelector('[data-el="providers"]')
422
+ providers: this.#root.querySelector('[data-el="providers"]'),
423
+ adapters: this.#root.querySelector('[data-el="adapters"]')
375
424
  };
376
425
 
377
426
  this.#root.querySelector('.bl-foot').addEventListener('click', (event) => {
@@ -383,6 +432,7 @@ export class UIOverlay {
383
432
  else if (act === 'live') this.core.restoreRealtime();
384
433
  else if (act === 'ask') this.core.ask();
385
434
  else if (act === 'assist') this.core.setActiveAssistant(btn.getAttribute('data-name'));
435
+ else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
386
436
  });
387
437
 
388
438
  this.#els.console.addEventListener('scroll', () => {
@@ -405,6 +455,9 @@ export class UIOverlay {
405
455
  this.#els = null;
406
456
  this.#mounted = false;
407
457
  this.#providerSig = '';
458
+ this.#adapterSig = '';
459
+ this.#clearFlashTimers();
460
+ this.#flashAt.clear();
408
461
  return this;
409
462
  }
410
463
 
@@ -423,6 +476,7 @@ export class UIOverlay {
423
476
  onPacket(packet) {
424
477
  if (!this.#els || !packet) return;
425
478
  if (packet.type === 'sys') return;
479
+ this.#flashAdapterButton(packet);
426
480
  if (packet.level || packet.type === 'log' || packet.type === 'warn' || packet.type === 'error') {
427
481
  this.#appendLine(packet);
428
482
  }
@@ -454,6 +508,7 @@ export class UIOverlay {
454
508
  this.#renderHud(state.hud);
455
509
  this.#syncButtons(state);
456
510
  this.#syncProviders(state);
511
+ this.#syncAdapters(state);
457
512
 
458
513
  if (this.#stickBottom) {
459
514
  this.#els.console.scrollTop = this.#els.console.scrollHeight;
@@ -529,6 +584,63 @@ export class UIOverlay {
529
584
  wrap.innerHTML = html;
530
585
  }
531
586
 
587
+ #syncAdapters(state) {
588
+ const wrap = this.#els.adapters;
589
+ if (!wrap) return;
590
+ const list = Array.isArray(state.adapters) ? state.adapters : [];
591
+ const sig = list.map((entry) => `${entry.name}\0${entry.label}\0${entry.enabled ? 1 : 0}`).join('\n');
592
+ if (sig === this.#adapterSig) return;
593
+ this.#adapterSig = sig;
594
+ let html = '';
595
+ for (let i = 0; i < list.length; i++) {
596
+ const entry = list[i];
597
+ const hot = entry.enabled ? 'true' : 'false';
598
+ html += `<button class="bl-adapter" data-act="adapter" data-name="${escapeHtml(entry.name)}" data-hot="${hot}" type="button" title="${escapeHtml(entry.label)}">${escapeHtml(entry.label)}</button>`;
599
+ }
600
+ wrap.innerHTML = html;
601
+ }
602
+
603
+ #flashAdapterButton(packet) {
604
+ if (packet.type !== 'warn' && packet.type !== 'telemetry') return;
605
+ const wrap = this.#els.adapters;
606
+ if (!wrap || !this.core || typeof this.core.getAdapters !== 'function') return;
607
+ const name = packet.source;
608
+ if (!this.core.getAdapters().some((entry) => entry.name === name)) return;
609
+
610
+ const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
611
+ ? CSS.escape(name)
612
+ : name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
613
+ const btn = wrap.querySelector(`[data-name="${escaped}"]`);
614
+ if (!btn) return;
615
+
616
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
617
+ const last = this.#flashAt.get(name) ?? 0;
618
+ if (now - last < 1500) return;
619
+ this.#flashAt.set(name, now);
620
+
621
+ btn.dataset.flash = 'false';
622
+ void btn.offsetWidth;
623
+ btn.dataset.flash = 'true';
624
+
625
+ const prev = this.#flashTimers.get(name);
626
+ if (prev && typeof clearTimeout === 'function') clearTimeout(prev);
627
+ if (typeof setTimeout !== 'function') return;
628
+ const timer = setTimeout(() => {
629
+ this.#flashTimers.delete(name);
630
+ if (btn.dataset.flash === 'true') btn.dataset.flash = 'false';
631
+ }, 560);
632
+ this.#flashTimers.set(name, timer);
633
+ }
634
+
635
+ #clearFlashTimers() {
636
+ if (typeof clearTimeout !== 'function') {
637
+ this.#flashTimers.clear();
638
+ return;
639
+ }
640
+ for (const timer of this.#flashTimers.values()) clearTimeout(timer);
641
+ this.#flashTimers.clear();
642
+ }
643
+
532
644
  #paintGraph(series) {
533
645
  const canvas = this.#els.graph;
534
646
  const ctx = canvas.getContext('2d');
package/src/index.d.ts CHANGED
@@ -14,7 +14,8 @@ export const WELL_KNOWN_TYPES: readonly [
14
14
  'error',
15
15
  'state',
16
16
  'sys',
17
- 'clear'
17
+ 'clear',
18
+ 'telemetry'
18
19
  ];
19
20
 
20
21
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
@@ -59,6 +60,7 @@ export interface CoreState {
59
60
  mode: string;
60
61
  assistant: string | null;
61
62
  assistants: string[];
63
+ adapters: AdapterInfo[];
62
64
  }
63
65
 
64
66
  export interface CoreOptions {
@@ -106,6 +108,18 @@ export interface AssistantProvider {
106
108
  complete(input: { question: string; snapshot: AssistantSnapshot }): unknown;
107
109
  }
108
110
 
111
+ export interface AdapterHooks {
112
+ enable(): void;
113
+ disable(): void;
114
+ label?: string;
115
+ }
116
+
117
+ export interface AdapterInfo {
118
+ name: string;
119
+ label: string;
120
+ enabled: boolean;
121
+ }
122
+
109
123
  export function formatSimTime(ms: number): string;
110
124
 
111
125
  export class BeeLadybugCore {
@@ -155,6 +169,11 @@ export class BeeLadybugCore {
155
169
  setAssistant(provider: AssistantProvider): this;
156
170
  clearAssistant(name?: string): this;
157
171
  ask(question?: string): Promise<string | null>;
172
+ registerAdapter(name: string, hooks: AdapterHooks): this;
173
+ unregisterAdapter(name: string): this;
174
+ getAdapters(): AdapterInfo[];
175
+ toggleAdapter(name: string): this;
176
+ setAdapterEnabled(name: string, enabled: boolean): this;
158
177
  }
159
178
 
160
179
  export class UIOverlay {
@@ -193,6 +212,7 @@ export interface CanvasAdapterOptions {
193
212
  entities?: CanvasEntity[];
194
213
  overlay?: boolean;
195
214
  computeCollisions?: boolean;
215
+ autoPump?: boolean;
196
216
  colorActive?: string;
197
217
  colorColliding?: string;
198
218
  colorInactive?: string;
@@ -209,11 +229,13 @@ export class CanvasAdapter {
209
229
  entities: CanvasEntity[];
210
230
  overlayEnabled: boolean;
211
231
  computeCollisions: boolean;
232
+ autoPump: boolean;
212
233
  frozen: boolean;
213
234
  timeScale: number;
214
235
  constructor(core: BeeLadybugCore, options?: CanvasAdapterOptions);
215
236
  attach(): this;
216
237
  detach(): this;
238
+ startAutoPump(): this;
217
239
  setCanvas(canvas: HTMLCanvasElement | null): this;
218
240
  setEntities(entities: CanvasEntity[]): this;
219
241
  pump(ctx?: CanvasRenderingContext2D): this;
@@ -250,6 +272,44 @@ export class WebDOMAdapter {
250
272
  pump(): this;
251
273
  }
252
274
 
275
+ export interface AntAdapterOptions {
276
+ root?: ParentNode | null;
277
+ threshold?: number;
278
+ windowMs?: number;
279
+ source?: string;
280
+ }
281
+
282
+ export const ANT_ADAPTER_DEFAULTS: Required<Omit<AntAdapterOptions, 'root'>>;
283
+
284
+ export class AntAdapter {
285
+ core: BeeLadybugCore | null;
286
+ root: ParentNode | null;
287
+ threshold: number;
288
+ windowMs: number;
289
+ source: string;
290
+ constructor(core: BeeLadybugCore, options?: AntAdapterOptions);
291
+ attach(): this;
292
+ detach(): this;
293
+ }
294
+
295
+ export interface SpiderAdapterOptions {
296
+ longTaskThreshold?: number;
297
+ resourceThreshold?: number;
298
+ source?: string;
299
+ }
300
+
301
+ export const SPIDER_ADAPTER_DEFAULTS: Required<SpiderAdapterOptions>;
302
+
303
+ export class SpiderAdapter {
304
+ core: BeeLadybugCore | null;
305
+ longTaskThreshold: number;
306
+ resourceThreshold: number;
307
+ source: string;
308
+ constructor(core: BeeLadybugCore, options?: SpiderAdapterOptions);
309
+ attach(): this;
310
+ detach(): this;
311
+ }
312
+
253
313
  export type PythonBridgeStatus = 'idle' | 'connecting' | 'open' | 'closed';
254
314
 
255
315
  export interface PythonBridgeOptions {
package/src/index.js CHANGED
@@ -2,4 +2,6 @@ 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 { AntAdapter, ANT_ADAPTER_DEFAULTS } from './adapters/AntAdapter.js';
6
+ export { SpiderAdapter, SPIDER_ADAPTER_DEFAULTS } from './adapters/SpiderAdapter.js';
5
7
  export { PythonBridge, PYTHON_BRIDGE_DEFAULTS } from './adapters/python/PythonBridge.js';