@kibee/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1831 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BookmarkMemory: () => BookmarkMemory,
24
+ BridgeClient: () => BridgeClient,
25
+ FlowController: () => FlowController,
26
+ HighlightRenderer: () => HighlightRenderer,
27
+ KiBeeApiClient: () => KiBeeApiClient,
28
+ KiBeeClient: () => KiBeeClient,
29
+ Locator: () => Locator,
30
+ TargetRegistry: () => TargetRegistry
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/api/KiBeeApiClient.ts
35
+ var KiBeeApiClient = class {
36
+ constructor(baseUrl, options = {}) {
37
+ this.baseUrl = baseUrl;
38
+ this.options = options;
39
+ }
40
+ baseUrl;
41
+ options;
42
+ async fetchFlow(id) {
43
+ return this.request(`/flows/${id}`);
44
+ }
45
+ async startSession(input) {
46
+ return this.request("/sessions", {
47
+ method: "POST",
48
+ body: JSON.stringify(input)
49
+ });
50
+ }
51
+ async resolveIntent(input) {
52
+ return this.request("/intent/resolve", {
53
+ method: "POST",
54
+ body: JSON.stringify(input)
55
+ });
56
+ }
57
+ async resolveRecovery(input) {
58
+ return this.request("/recovery/resolve", {
59
+ method: "POST",
60
+ body: JSON.stringify(input)
61
+ });
62
+ }
63
+ async trackEvent(event) {
64
+ await this.request("/events", {
65
+ method: "POST",
66
+ body: JSON.stringify(event)
67
+ });
68
+ }
69
+ /**
70
+ * Unified visitor poll. One round-trip pulls messages + pending pushes
71
+ * + mode + friction. Replaces the previous fetchVisitorMessages +
72
+ * /admin/sessions/:id/pending pair for visitors using publishable-key
73
+ * auth. The legacy fetchVisitorMessages remains for shipped SDK
74
+ * builds that haven't been updated yet.
75
+ */
76
+ async fetchVisitorSync(sessionId, since) {
77
+ const params = new URLSearchParams({ sessionId });
78
+ if (since) params.set("since", since);
79
+ return this.request(`/v1/visitor/sync?${params.toString()}`);
80
+ }
81
+ /**
82
+ * Legacy split poll — kept for backward compat. Prefer fetchVisitorSync.
83
+ */
84
+ async fetchVisitorMessages(sessionId, since) {
85
+ const params = new URLSearchParams({ sessionId });
86
+ if (since) params.set("since", since);
87
+ return this.request(`/v1/visitor/messages?${params.toString()}`);
88
+ }
89
+ /**
90
+ * Visitor (or distress detector) requests a human. Sets
91
+ * `requested_human_at`, generates an AI summary for the agent, and lights
92
+ * up the "Needs help" badge in admin Hive.
93
+ */
94
+ async escalateToHuman(sessionId) {
95
+ await this.request("/v1/visitor/escalate", {
96
+ method: "POST",
97
+ body: JSON.stringify({ sessionId })
98
+ });
99
+ }
100
+ /**
101
+ * Open the visitor's per-session SSE stream. EventSource can't pass auth
102
+ * headers, so the publishable key + websiteId ride along as query params.
103
+ * Caller wires `onmessage` and is responsible for re-opening on disconnect.
104
+ *
105
+ * Returns null when EventSource isn't available (older browsers / strict
106
+ * proxies) — caller falls back to polling `fetchVisitorMessages`.
107
+ */
108
+ openVisitorStream(sessionId) {
109
+ if (typeof EventSource === "undefined") return null;
110
+ const websiteId = this.options.websiteId;
111
+ const token = this.options.publicKey;
112
+ if (!websiteId || !token) return null;
113
+ const params = new URLSearchParams({
114
+ sessionId,
115
+ websiteId,
116
+ // The token query param is necessary because EventSource doesn't accept
117
+ // headers. The query string is logged on the server but the token never
118
+ // shows up in `logRequest` (the visitor stream is special-cased there).
119
+ token
120
+ });
121
+ return new EventSource(`${this.baseUrl}/v1/visitor/stream?${params.toString()}`);
122
+ }
123
+ buildHeaders(extra) {
124
+ const headers = {
125
+ "content-type": "application/json",
126
+ ...extra
127
+ };
128
+ if (this.options.websiteId && this.options.publicKey) {
129
+ headers["x-kibee-website-id"] = this.options.websiteId;
130
+ headers.authorization = `Bearer ${this.options.publicKey}`;
131
+ } else if (this.options.legacyApiKey) {
132
+ headers["x-kibee-key"] = this.options.legacyApiKey;
133
+ }
134
+ return headers;
135
+ }
136
+ async request(path, init = {}) {
137
+ const headers = this.buildHeaders(init.headers);
138
+ const response = await fetch(`${this.baseUrl}${path}`, {
139
+ ...init,
140
+ headers,
141
+ cache: "no-store"
142
+ });
143
+ if (!response.ok) {
144
+ const body = await response.text();
145
+ throw new Error(body || `Request failed: ${response.status}`);
146
+ }
147
+ if (response.status === 204) {
148
+ return void 0;
149
+ }
150
+ return await response.json();
151
+ }
152
+ };
153
+
154
+ // src/bridge/BridgeClient.ts
155
+ var import_contracts = require("@kibee/contracts");
156
+ var BridgeClient = class {
157
+ ws = null;
158
+ handlers = /* @__PURE__ */ new Map();
159
+ opts;
160
+ state = "disconnected";
161
+ heartbeatTimer;
162
+ lastPongAt = 0;
163
+ constructor(opts) {
164
+ this.opts = opts;
165
+ }
166
+ on(eventType, handler) {
167
+ const list = this.handlers.get(eventType) ?? [];
168
+ list.push(handler);
169
+ this.handlers.set(eventType, list);
170
+ return () => {
171
+ const next = (this.handlers.get(eventType) ?? []).filter(
172
+ (h) => h !== handler
173
+ );
174
+ this.handlers.set(eventType, next);
175
+ };
176
+ }
177
+ async connect() {
178
+ const { token } = await this.opts.tokenFetcher();
179
+ const urlWithToken = `${this.opts.bridgeUrl}?token=${encodeURIComponent(token)}`;
180
+ const ws = new WebSocket(urlWithToken);
181
+ this.ws = ws;
182
+ await new Promise((resolve, reject) => {
183
+ ws.addEventListener("open", () => resolve());
184
+ ws.addEventListener("error", (e) => reject(e));
185
+ });
186
+ ws.addEventListener("message", (e) => {
187
+ this.lastPongAt = Date.now();
188
+ let parsed;
189
+ try {
190
+ parsed = JSON.parse(typeof e.data === "string" ? e.data : "");
191
+ } catch {
192
+ return;
193
+ }
194
+ if (!(0, import_contracts.isBridgeEnvelope)(parsed)) return;
195
+ const list = this.handlers.get(parsed.type) ?? [];
196
+ list.forEach((fn) => fn(parsed));
197
+ });
198
+ this.send({
199
+ v: 1,
200
+ source: this.opts.source,
201
+ target: "broadcast",
202
+ tenantId: null,
203
+ workspaceId: null,
204
+ visitorId: this.opts.visitorId,
205
+ sessionId: null,
206
+ type: "presence:hello",
207
+ ts: Date.now(),
208
+ payload: {}
209
+ });
210
+ this.startHeartbeat();
211
+ }
212
+ startHeartbeat() {
213
+ if (this.heartbeatTimer) {
214
+ clearInterval(this.heartbeatTimer);
215
+ this.heartbeatTimer = void 0;
216
+ }
217
+ this.lastPongAt = Date.now();
218
+ this.heartbeatTimer = setInterval(() => {
219
+ if (this.ws?.readyState === 1) {
220
+ try {
221
+ this.ws.send(
222
+ JSON.stringify({
223
+ v: 1,
224
+ source: this.opts.source,
225
+ target: "broadcast",
226
+ tenantId: null,
227
+ workspaceId: null,
228
+ visitorId: this.opts.visitorId,
229
+ sessionId: null,
230
+ type: "presence:hello",
231
+ ts: Date.now(),
232
+ payload: { ping: true }
233
+ })
234
+ );
235
+ } catch {
236
+ }
237
+ }
238
+ if (Date.now() - this.lastPongAt > 12e3) {
239
+ this.markDegraded();
240
+ }
241
+ }, 1e4);
242
+ }
243
+ markDegraded() {
244
+ if (this.heartbeatTimer) {
245
+ clearInterval(this.heartbeatTimer);
246
+ this.heartbeatTimer = void 0;
247
+ }
248
+ if (typeof window !== "undefined") {
249
+ window.dispatchEvent(new CustomEvent("kibee-bridge-degraded"));
250
+ }
251
+ this.state = "disconnected";
252
+ this.ws?.close();
253
+ }
254
+ send(envelope) {
255
+ if (!this.ws) throw new Error("BridgeClient: not connected");
256
+ const OPEN = this.ws.constructor.OPEN;
257
+ if (this.ws.readyState !== OPEN) return;
258
+ this.ws.send(JSON.stringify(envelope));
259
+ }
260
+ close() {
261
+ if (this.heartbeatTimer) {
262
+ clearInterval(this.heartbeatTimer);
263
+ this.heartbeatTimer = void 0;
264
+ }
265
+ if (!this.ws) return;
266
+ try {
267
+ this.send({
268
+ v: 1,
269
+ source: this.opts.source,
270
+ target: "broadcast",
271
+ tenantId: null,
272
+ workspaceId: null,
273
+ visitorId: this.opts.visitorId,
274
+ sessionId: null,
275
+ type: "presence:bye",
276
+ ts: Date.now(),
277
+ payload: {}
278
+ });
279
+ } catch {
280
+ }
281
+ this.ws.close();
282
+ this.ws = null;
283
+ this.state = "disconnected";
284
+ }
285
+ /**
286
+ * Wraps `connect()` with exponential backoff. Defaults: 10 attempts,
287
+ * 1s base, 30s cap (delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s, ...). Adds
288
+ * 0–20% jitter per attempt.
289
+ *
290
+ * On success, `state` is `connected` and the method resolves.
291
+ * After `maxAttempts` failures, `state` is `failed` and the last error
292
+ * is re-thrown.
293
+ *
294
+ * `connect()` itself remains a single-attempt method so existing callers
295
+ * are unaffected.
296
+ */
297
+ async connectWithReconnect(opts = {}) {
298
+ const max = opts.maxAttempts ?? 10;
299
+ const base = opts.baseDelayMs ?? 1e3;
300
+ const cap = opts.capDelayMs ?? 3e4;
301
+ let attempt = 0;
302
+ while (true) {
303
+ try {
304
+ this.state = "connecting";
305
+ await this.connect();
306
+ this.state = "connected";
307
+ return;
308
+ } catch (err) {
309
+ attempt += 1;
310
+ if (attempt >= max) {
311
+ this.state = "failed";
312
+ throw err;
313
+ }
314
+ const delay = Math.min(cap, base * Math.pow(2, attempt - 1));
315
+ const jitter = delay * 0.2 * Math.random();
316
+ await new Promise((r) => setTimeout(r, delay + jitter));
317
+ }
318
+ }
319
+ }
320
+ };
321
+
322
+ // src/controllers/BookmarkMemory.ts
323
+ var BookmarkMemory = class {
324
+ storageKey = "kibee:bookmarks";
325
+ list() {
326
+ return this.read();
327
+ }
328
+ create(input) {
329
+ const now = (/* @__PURE__ */ new Date()).toISOString();
330
+ const bookmark = {
331
+ id: this.generateId(),
332
+ kind: input.kind,
333
+ label: input.label,
334
+ note: input.note,
335
+ pageUrl: window.location.href,
336
+ pageTitle: document.title,
337
+ target: input.target,
338
+ metadata: input.metadata,
339
+ createdAt: now,
340
+ updatedAt: now
341
+ };
342
+ const current = this.read();
343
+ current.push(bookmark);
344
+ this.write(current);
345
+ return bookmark;
346
+ }
347
+ update(id, patch) {
348
+ const current = this.read();
349
+ const index = current.findIndex((x) => x.id === id);
350
+ if (index === -1) return null;
351
+ current[index] = {
352
+ ...current[index],
353
+ ...patch,
354
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
355
+ };
356
+ this.write(current);
357
+ return current[index];
358
+ }
359
+ remove(id) {
360
+ const current = this.read().filter((x) => x.id !== id);
361
+ this.write(current);
362
+ }
363
+ findByPage(url = window.location.href) {
364
+ return this.read().filter((x) => x.pageUrl === url);
365
+ }
366
+ read() {
367
+ try {
368
+ const raw = localStorage.getItem(this.storageKey);
369
+ if (!raw) return [];
370
+ const parsed = JSON.parse(raw);
371
+ return Array.isArray(parsed) ? parsed : [];
372
+ } catch {
373
+ return [];
374
+ }
375
+ }
376
+ write(value) {
377
+ localStorage.setItem(this.storageKey, JSON.stringify(value));
378
+ }
379
+ generateId() {
380
+ return `fbm_${Math.random().toString(36).slice(2, 10)}`;
381
+ }
382
+ };
383
+
384
+ // src/controllers/FlowController.ts
385
+ var FlowController = class {
386
+ constructor(bee, locator, highlight, bookmarks = new BookmarkMemory(), onCommand) {
387
+ this.bee = bee;
388
+ this.locator = locator;
389
+ this.highlight = highlight;
390
+ this.bookmarks = bookmarks;
391
+ this.onCommand = onCommand;
392
+ }
393
+ bee;
394
+ locator;
395
+ highlight;
396
+ bookmarks;
397
+ onCommand;
398
+ async init() {
399
+ this.highlight.mount();
400
+ await this.bee.dock();
401
+ }
402
+ async runFlow(flow) {
403
+ await this.bee.undock();
404
+ if (flow.introMessage) {
405
+ this.bee.speak(flow.introMessage);
406
+ }
407
+ for (const command of flow.steps) {
408
+ this.onCommand?.(command, "started");
409
+ const success = await this.execute(command);
410
+ this.onCommand?.(
411
+ command,
412
+ success ? "completed" : "missing_target"
413
+ );
414
+ }
415
+ }
416
+ /**
417
+ * Interactive step-through mode: bee flies to each `fly_to` step and shows
418
+ * prev / next buttons in the tooltip so the user controls the pace.
419
+ */
420
+ async runInteractiveFlow(flow) {
421
+ const guidedSteps = flow.steps.filter(
422
+ (s) => s.type === "fly_to"
423
+ );
424
+ if (guidedSteps.length === 0) {
425
+ await this.runFlow(flow);
426
+ return;
427
+ }
428
+ await this.bee.undock();
429
+ const showStep = async (index) => {
430
+ const step = guidedSteps[index];
431
+ const isFirst = index === 0;
432
+ const isLast = index === guidedSteps.length - 1;
433
+ await this.guideTo(
434
+ step.target,
435
+ step.message,
436
+ step.highlight,
437
+ step.moveTo,
438
+ {
439
+ onPrev: isFirst ? null : () => {
440
+ void showStep(index - 1);
441
+ },
442
+ onNext: isLast ? () => {
443
+ this.clearFocus();
444
+ } : () => {
445
+ void showStep(index + 1);
446
+ },
447
+ stepLabel: `${index + 1} / ${guidedSteps.length}`
448
+ }
449
+ );
450
+ };
451
+ await showStep(0);
452
+ }
453
+ clearFocus() {
454
+ this.highlight.hide();
455
+ this.bee.hideMessage();
456
+ this.bee.hideActions();
457
+ this.bee.setState("idle");
458
+ void this.bee.dock();
459
+ }
460
+ async createHoneyBookmark(label, target, note) {
461
+ return this.createBookmark("honey", label, target, note);
462
+ }
463
+ async createStingBookmark(label, target, note) {
464
+ return this.createBookmark("sting", label, target, note);
465
+ }
466
+ listBookmarks() {
467
+ return this.bookmarks.list();
468
+ }
469
+ getPageBookmarks() {
470
+ return this.bookmarks.findByPage();
471
+ }
472
+ async revisitBookmark(bookmark) {
473
+ const kindMessage = bookmark.kind === "honey" ? `Here\u2019s your honey mark: ${bookmark.label}` : `Here\u2019s your sting mark: ${bookmark.label}`;
474
+ await this.guideTo(
475
+ bookmark.target,
476
+ bookmark.note ? `${kindMessage}. ${bookmark.note}` : kindMessage,
477
+ bookmark.kind === "honey" ? "soft" : "ring"
478
+ );
479
+ }
480
+ removeBookmark(id) {
481
+ this.bookmarks.remove(id);
482
+ }
483
+ async execute(command) {
484
+ switch (command.type) {
485
+ case "speak":
486
+ this.bee.speak(command.message);
487
+ return true;
488
+ case "celebrate":
489
+ this.bee.setState("celebrating");
490
+ if (command.message) {
491
+ this.bee.speak(command.message);
492
+ }
493
+ return true;
494
+ case "end_flow":
495
+ if (command.message) {
496
+ this.bee.speak(command.message);
497
+ }
498
+ if (command.status === "completed") {
499
+ this.bee.setState("celebrating");
500
+ await this.finishAndDock(860);
501
+ } else {
502
+ this.bee.setState("idle");
503
+ await this.bee.dock();
504
+ }
505
+ this.highlight.hide();
506
+ return true;
507
+ case "highlight": {
508
+ const resolved = this.locator.resolve(command.target);
509
+ if (!resolved) {
510
+ this.notifyMissingTarget();
511
+ return false;
512
+ }
513
+ this.highlight.show(resolved.rect, command.variant ?? "pulse");
514
+ return true;
515
+ }
516
+ case "fly_to":
517
+ return this.guideTo(command.target, command.message, command.highlight, command.moveTo);
518
+ case "wait_for_click": {
519
+ const resolved = this.locator.resolve(command.target);
520
+ if (!resolved) {
521
+ this.notifyMissingTarget();
522
+ return false;
523
+ }
524
+ await this.waitForClick(resolved.element, command.timeoutMs ?? 2e4);
525
+ return true;
526
+ }
527
+ case "wait_for_input": {
528
+ const resolved = this.locator.resolve(command.target);
529
+ if (!resolved) {
530
+ this.notifyMissingTarget();
531
+ return false;
532
+ }
533
+ await this.waitForInput(resolved.element, command.timeoutMs ?? 2e4);
534
+ return true;
535
+ }
536
+ default:
537
+ return false;
538
+ }
539
+ }
540
+ async createBookmark(kind, label, target, note) {
541
+ const resolved = this.locator.resolve(target);
542
+ if (!resolved) {
543
+ this.bee.setState("thinking");
544
+ this.bee.speak("I couldn\u2019t save that spot because I couldn\u2019t find it.");
545
+ void this.finishAndDock(900);
546
+ return null;
547
+ }
548
+ const bookmark = this.bookmarks.create({
549
+ kind,
550
+ label,
551
+ note,
552
+ target
553
+ });
554
+ const placement = this.resolveGuidePlacement(resolved.rect);
555
+ await this.bee.moveTo(resolved.rect.right, resolved.rect.top, placement.moveTo);
556
+ this.bee.setState("celebrating");
557
+ this.bee.speak(
558
+ kind === "honey" ? `Saved bookmark: ${label}` : `Issue flagged: ${label}`,
559
+ placement.speak
560
+ );
561
+ await this.finishInPlace(920);
562
+ return bookmark;
563
+ }
564
+ async guideTo(target, message, highlight, moveTo, navOptions) {
565
+ await this.bee.undock();
566
+ const resolved = this.locator.resolve(target);
567
+ if (!resolved) {
568
+ this.notifyMissingTarget();
569
+ return false;
570
+ }
571
+ this.locator.scrollIntoView(target);
572
+ await this.delay(350);
573
+ const fresh = this.locator.resolve(target);
574
+ if (!fresh) {
575
+ this.notifyMissingTarget();
576
+ return false;
577
+ }
578
+ if (highlight) {
579
+ this.highlight.show(fresh.rect, highlight);
580
+ }
581
+ const anchorX = fresh.rect.right;
582
+ const anchorY = fresh.rect.top;
583
+ const placement = this.resolveGuidePlacement(fresh.rect, moveTo);
584
+ await this.bee.moveTo(anchorX, anchorY, {
585
+ ...placement.moveTo,
586
+ offsetX: placement.moveTo.offsetX ?? 16,
587
+ offsetY: placement.moveTo.offsetY ?? -18
588
+ });
589
+ if (message) {
590
+ this.bee.speak(message, { ...placement.speak, ...navOptions });
591
+ }
592
+ if (!navOptions) {
593
+ this.bee.showActions((choice) => {
594
+ if (choice === "honey") {
595
+ void this.createHoneyBookmark("Quick honey", target, message);
596
+ } else {
597
+ void this.createStingBookmark("Quick sting", target, message);
598
+ }
599
+ this.bee.hideActions();
600
+ });
601
+ }
602
+ return true;
603
+ }
604
+ waitForClick(el, timeoutMs) {
605
+ return new Promise((resolve) => {
606
+ let finished = false;
607
+ const cleanup = () => {
608
+ el.removeEventListener("click", onClick);
609
+ clearTimeout(timer);
610
+ };
611
+ const onClick = () => {
612
+ if (finished) return;
613
+ finished = true;
614
+ cleanup();
615
+ resolve();
616
+ };
617
+ const timer = window.setTimeout(() => {
618
+ if (finished) return;
619
+ finished = true;
620
+ cleanup();
621
+ resolve();
622
+ }, timeoutMs);
623
+ el.addEventListener("click", onClick, { once: true });
624
+ });
625
+ }
626
+ waitForInput(el, timeoutMs) {
627
+ return new Promise((resolve) => {
628
+ let finished = false;
629
+ const cleanup = () => {
630
+ el.removeEventListener("input", onInput);
631
+ el.removeEventListener("change", onInput);
632
+ clearTimeout(timer);
633
+ };
634
+ const onInput = () => {
635
+ if (finished) return;
636
+ finished = true;
637
+ cleanup();
638
+ resolve();
639
+ };
640
+ const timer = window.setTimeout(() => {
641
+ if (finished) return;
642
+ finished = true;
643
+ cleanup();
644
+ resolve();
645
+ }, timeoutMs);
646
+ el.addEventListener("input", onInput, { once: true });
647
+ el.addEventListener("change", onInput, { once: true });
648
+ });
649
+ }
650
+ delay(ms) {
651
+ return new Promise((resolve) => setTimeout(resolve, ms));
652
+ }
653
+ resolveGuidePlacement(rect, overrides) {
654
+ const viewportWidth = window.innerWidth;
655
+ const viewportHeight = window.innerHeight;
656
+ const anchorX = rect.right;
657
+ const anchorY = rect.top;
658
+ const side = this.pickGuideSide(rect, viewportWidth, viewportHeight);
659
+ const base = this.resolveTopRightBeeOffset(rect, side, viewportWidth, viewportHeight);
660
+ const offsetX = overrides?.offsetX ?? base.offsetX;
661
+ const offsetY = overrides?.offsetY ?? base.offsetY;
662
+ const beeX = anchorX + offsetX;
663
+ const beeY = anchorY + offsetY;
664
+ const tooltip = this.resolveTooltipPosition(
665
+ side,
666
+ beeX,
667
+ beeY,
668
+ viewportWidth,
669
+ viewportHeight,
670
+ rect
671
+ );
672
+ return {
673
+ moveTo: {
674
+ ...overrides,
675
+ offsetX,
676
+ offsetY
677
+ },
678
+ speak: {
679
+ ...tooltip
680
+ }
681
+ };
682
+ }
683
+ pickGuideSide(rect, viewportWidth, viewportHeight) {
684
+ const space = {
685
+ left: rect.left,
686
+ right: viewportWidth - rect.right,
687
+ top: rect.top,
688
+ bottom: viewportHeight - rect.bottom
689
+ };
690
+ const wideTarget = rect.width > viewportWidth * 0.32;
691
+ const tallTarget = rect.height > 140;
692
+ if (wideTarget || tallTarget) {
693
+ if (Math.max(space.left, space.right) >= 180) {
694
+ return space.left >= space.right ? "left" : "right";
695
+ }
696
+ if (space.top >= space.bottom) {
697
+ return "top";
698
+ }
699
+ return "bottom";
700
+ }
701
+ if (space.right >= 320) return "right";
702
+ if (space.left >= 320) return "left";
703
+ if (space.top >= 220) return "top";
704
+ if (space.bottom >= 220) return "bottom";
705
+ return Object.entries(space).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "right";
706
+ }
707
+ resolveTopRightBeeOffset(rect, _side, viewportWidth, viewportHeight) {
708
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
709
+ const beeX = clamp(rect.right + 24, 88, viewportWidth - 88);
710
+ const beeY = clamp(rect.top - 28, 64, viewportHeight - 96);
711
+ return {
712
+ offsetX: beeX - rect.right,
713
+ offsetY: beeY - rect.top
714
+ };
715
+ }
716
+ /** Area of intersection of two axis-aligned rectangles (px²). */
717
+ rectOverlapArea(a, b) {
718
+ const w = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
719
+ const h = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
720
+ return w * h;
721
+ }
722
+ /**
723
+ * Place the speech bubble with a healthy gap from the bee and pick left-vs-right
724
+ * by scoring overlap with the bee body and the highlighted target.
725
+ */
726
+ resolveTooltipPosition(side, beeX, beeY, viewportWidth, viewportHeight, targetRect) {
727
+ const tooltipWidth = viewportWidth < 1100 ? 220 : 260;
728
+ const EST_H = 132;
729
+ const CLEAR = 56;
730
+ const BEE_R = 44;
731
+ const M = 16;
732
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
733
+ const beeBox = {
734
+ left: beeX - BEE_R,
735
+ right: beeX + BEE_R,
736
+ top: beeY - BEE_R,
737
+ bottom: beeY + BEE_R
738
+ };
739
+ const targetBox = {
740
+ left: targetRect.left,
741
+ top: targetRect.top,
742
+ right: targetRect.right,
743
+ bottom: targetRect.bottom
744
+ };
745
+ const tooltipBox = (xRaw, y2) => {
746
+ const x2 = clamp(xRaw, M, viewportWidth - tooltipWidth - M);
747
+ const top = y2 - EST_H / 2;
748
+ const bottom = y2 + EST_H / 2;
749
+ return {
750
+ left: x2,
751
+ right: x2 + tooltipWidth,
752
+ top,
753
+ bottom
754
+ };
755
+ };
756
+ const scoreHorizontal = (xRaw, y2) => {
757
+ const t = tooltipBox(xRaw, y2);
758
+ const beeOverlap = this.rectOverlapArea(t, beeBox);
759
+ const targetOverlap = this.rectOverlapArea(t, targetBox);
760
+ const clampDelta = Math.abs(
761
+ clamp(xRaw, M, viewportWidth - tooltipWidth - M) - xRaw
762
+ );
763
+ return beeOverlap * 8e3 + targetOverlap * 4 + clampDelta * 2;
764
+ };
765
+ let x;
766
+ let y;
767
+ let arrowEdge;
768
+ if (side === "top") {
769
+ x = beeX - tooltipWidth * 0.56;
770
+ y = beeY - 56;
771
+ arrowEdge = "bottom";
772
+ } else if (side === "bottom") {
773
+ x = beeX - tooltipWidth * 0.48;
774
+ y = beeY + 26;
775
+ arrowEdge = "top";
776
+ } else {
777
+ const yH = beeY - 6;
778
+ const xLeft = beeX - tooltipWidth - CLEAR;
779
+ const xRight = beeX + CLEAR;
780
+ const sL = scoreHorizontal(xLeft, yH);
781
+ const sR = scoreHorizontal(xRight, yH);
782
+ const preferLeftOfBee = side === "left";
783
+ const tieEps = 500;
784
+ if (sL + tieEps < sR) {
785
+ x = xLeft;
786
+ y = yH;
787
+ arrowEdge = "right";
788
+ } else if (sR + tieEps < sL) {
789
+ x = xRight;
790
+ y = yH;
791
+ arrowEdge = "left";
792
+ } else if (preferLeftOfBee) {
793
+ x = xLeft;
794
+ y = yH;
795
+ arrowEdge = "right";
796
+ } else {
797
+ x = xRight;
798
+ y = yH;
799
+ arrowEdge = "left";
800
+ }
801
+ }
802
+ return {
803
+ x: clamp(x, M, viewportWidth - tooltipWidth - M),
804
+ y: clamp(y, 64, viewportHeight - 64),
805
+ maxWidth: tooltipWidth,
806
+ arrowEdge
807
+ };
808
+ }
809
+ notifyMissingTarget() {
810
+ this.bee.setState("thinking");
811
+ this.bee.speak("I couldn't find that spot on this page.");
812
+ void this.finishAndDock(1e3);
813
+ }
814
+ async finishAndDock(delayMs) {
815
+ await this.delay(delayMs);
816
+ this.bee.hideActions();
817
+ this.bee.setState("idle");
818
+ await this.bee.dock();
819
+ }
820
+ /** Like finishAndDock but without docking — bee stays wherever it landed. */
821
+ async finishInPlace(delayMs) {
822
+ await this.delay(delayMs);
823
+ this.bee.hideActions();
824
+ this.bee.setState("idle");
825
+ }
826
+ };
827
+
828
+ // src/dom/captureContext.ts
829
+ var MAX_TEXT = 80;
830
+ var MAX_PROXIMITY_PX = 240;
831
+ var INTERACTIVE_SELECTOR = 'a[href], button, input, select, textarea, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="checkbox"], [role="radio"], [role="switch"], [contenteditable=""], [contenteditable="true"], [data-kibee-id]';
832
+ function shortText(s) {
833
+ if (!s) return null;
834
+ const cleaned = s.replace(/\s+/g, " ").trim();
835
+ if (!cleaned) return null;
836
+ return cleaned.length > MAX_TEXT ? cleaned.slice(0, MAX_TEXT - 1) + "\u2026" : cleaned;
837
+ }
838
+ function visibleText(el) {
839
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
840
+ return shortText(el.placeholder || el.getAttribute("aria-label"));
841
+ }
842
+ if (el instanceof HTMLSelectElement) {
843
+ return shortText(el.getAttribute("aria-label"));
844
+ }
845
+ return shortText(el.textContent);
846
+ }
847
+ function elementRect(el) {
848
+ const r = el.getBoundingClientRect();
849
+ return { x: r.left, y: r.top, width: r.width, height: r.height };
850
+ }
851
+ function distanceToCentre(rect, x, y) {
852
+ const cx = rect.left + rect.width / 2;
853
+ const cy = rect.top + rect.height / 2;
854
+ return Math.hypot(cx - x, cy - y);
855
+ }
856
+ function selectorPath(target) {
857
+ const segments = [];
858
+ let el = target;
859
+ for (let i = 0; i < 6 && el && el !== document.body; i++) {
860
+ const tag = el.tagName.toLowerCase();
861
+ let seg = tag;
862
+ if (el.id) {
863
+ seg += `#${el.id}`;
864
+ segments.unshift(seg);
865
+ break;
866
+ }
867
+ const cls = (el.getAttribute("class") || "").split(/\s+/).filter((c) => c && !c.startsWith("kibee-")).slice(0, 2).join(".");
868
+ if (cls) seg += `.${cls}`;
869
+ segments.unshift(seg);
870
+ el = el.parentElement;
871
+ }
872
+ return segments.join(" > ");
873
+ }
874
+ function describeNearest(el, beeX, beeY) {
875
+ return {
876
+ tag: el.tagName.toLowerCase(),
877
+ text: visibleText(el),
878
+ role: el.getAttribute("role") || null,
879
+ kibeeId: el.getAttribute("data-kibee-id"),
880
+ testId: el.getAttribute("data-testid"),
881
+ domId: el.id || null,
882
+ selectorPath: selectorPath(el),
883
+ distancePx: Math.round(distanceToCentre(el.getBoundingClientRect(), beeX, beeY)),
884
+ rect: elementRect(el)
885
+ };
886
+ }
887
+ function findNearestInteractive(beeX, beeY) {
888
+ const stack = document.elementsFromPoint?.(beeX, beeY) ?? [];
889
+ for (const el of stack) {
890
+ if (el.matches?.(INTERACTIVE_SELECTOR)) return el;
891
+ const closest = el.closest?.(INTERACTIVE_SELECTOR);
892
+ if (closest) return closest;
893
+ }
894
+ let best = null;
895
+ const candidates = document.querySelectorAll(INTERACTIVE_SELECTOR);
896
+ for (const el of candidates) {
897
+ const r = el.getBoundingClientRect();
898
+ if (r.width === 0 && r.height === 0) continue;
899
+ if (r.bottom < 0 || r.top > window.innerHeight) continue;
900
+ if (r.right < 0 || r.left > window.innerWidth) continue;
901
+ const dist = distanceToCentre(r, beeX, beeY);
902
+ if (dist > MAX_PROXIMITY_PX) continue;
903
+ if (!best || dist < best.dist) best = { el, dist };
904
+ }
905
+ return best?.el ?? null;
906
+ }
907
+ function sampleNearbyText(beeX, beeY) {
908
+ const snippets = [];
909
+ const seen = /* @__PURE__ */ new Set();
910
+ const candidates = document.querySelectorAll(
911
+ "h1, h2, h3, h4, p, label, span, li, td, th, button"
912
+ );
913
+ for (const el of candidates) {
914
+ const r = el.getBoundingClientRect();
915
+ if (r.width === 0 && r.height === 0) continue;
916
+ const dist = distanceToCentre(r, beeX, beeY);
917
+ if (dist > MAX_PROXIMITY_PX) continue;
918
+ const t = visibleText(el);
919
+ if (!t || seen.has(t)) continue;
920
+ seen.add(t);
921
+ snippets.push(t);
922
+ if (snippets.length >= 6) break;
923
+ }
924
+ return snippets;
925
+ }
926
+ function captureContext(input) {
927
+ const { beeX, beeY } = input;
928
+ const nearest = findNearestInteractive(beeX, beeY);
929
+ return {
930
+ pathname: location.pathname,
931
+ search: location.search,
932
+ hash: location.hash,
933
+ title: shortText(document.title) ?? "",
934
+ beePosition: { x: Math.round(beeX), y: Math.round(beeY) },
935
+ viewport: {
936
+ width: window.innerWidth,
937
+ height: window.innerHeight,
938
+ dpr: window.devicePixelRatio || 1
939
+ },
940
+ scroll: { x: window.scrollX, y: window.scrollY },
941
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
942
+ nearestElement: nearest ? describeNearest(nearest, beeX, beeY) : null,
943
+ nearbyText: sampleNearbyText(beeX, beeY)
944
+ };
945
+ }
946
+
947
+ // src/dom/Locator.ts
948
+ var Locator = class {
949
+ resolve(target) {
950
+ const element = this.findElement(target);
951
+ if (!element) return null;
952
+ const rect = element.getBoundingClientRect();
953
+ return {
954
+ element,
955
+ rect,
956
+ centerX: rect.left + rect.width / 2,
957
+ centerY: rect.top + rect.height / 2
958
+ };
959
+ }
960
+ findElement(target) {
961
+ if (target.selector) {
962
+ const el = document.querySelector(target.selector);
963
+ if (el instanceof HTMLElement) return el;
964
+ }
965
+ const kibeeId = target.kibeeId ?? target.beeId;
966
+ if (kibeeId) {
967
+ const el = document.querySelector(`[data-kibee-id="${kibeeId}"]`);
968
+ if (el instanceof HTMLElement) return el;
969
+ }
970
+ if (target.textHints?.length) {
971
+ const candidates = Array.from(
972
+ document.querySelectorAll(
973
+ "button, a, input, textarea, select, [role='button'], [data-kibee-id]"
974
+ )
975
+ );
976
+ for (const node of candidates) {
977
+ if (!(node instanceof HTMLElement)) continue;
978
+ const text = this.getSearchableText(node).toLowerCase();
979
+ const matched = target.textHints.some(
980
+ (hint) => text.includes(hint.toLowerCase())
981
+ );
982
+ if (matched) return node;
983
+ }
984
+ }
985
+ return null;
986
+ }
987
+ scrollIntoView(target, behavior = "smooth") {
988
+ const resolved = this.resolve(target);
989
+ if (!resolved) return;
990
+ resolved.element.scrollIntoView({
991
+ behavior,
992
+ block: "center",
993
+ inline: "center"
994
+ });
995
+ }
996
+ getSearchableText(el) {
997
+ const bits = [
998
+ el.textContent ?? "",
999
+ el.getAttribute("aria-label") ?? "",
1000
+ el.getAttribute("placeholder") ?? "",
1001
+ el.getAttribute("title") ?? "",
1002
+ el.getAttribute("data-kibee-id") ?? ""
1003
+ ];
1004
+ return bits.join(" ").trim();
1005
+ }
1006
+ };
1007
+
1008
+ // src/dom/TargetRegistry.ts
1009
+ var TargetRegistry = class {
1010
+ targets = [];
1011
+ set(targets) {
1012
+ this.targets = [...targets];
1013
+ }
1014
+ list() {
1015
+ return [...this.targets];
1016
+ }
1017
+ getVisibleTargets(doc = document) {
1018
+ return this.targets.reduce((visible, target) => {
1019
+ const kibeeId = target.target.kibeeId ?? target.target.beeId;
1020
+ const node = kibeeId && doc.querySelector(`[data-kibee-id="${kibeeId}"]`) || (target.target.selector ? doc.querySelector(target.target.selector) : null);
1021
+ if (!node) return visible;
1022
+ const matchedText = [
1023
+ node.textContent ?? "",
1024
+ node.getAttribute("aria-label") ?? "",
1025
+ node.getAttribute("placeholder") ?? ""
1026
+ ].join(" ").trim();
1027
+ visible.push({
1028
+ ...target,
1029
+ matchedText
1030
+ });
1031
+ return visible;
1032
+ }, []);
1033
+ }
1034
+ };
1035
+
1036
+ // src/renderers/HighlightRenderer.ts
1037
+ var HighlightRenderer = class {
1038
+ el = null;
1039
+ mount(container = document.body) {
1040
+ if (this.el) return;
1041
+ this.el = document.createElement("div");
1042
+ this.el.className = "kibee-highlight kibee-highlight--hidden";
1043
+ container.appendChild(this.el);
1044
+ }
1045
+ show(rect, variant = "pulse") {
1046
+ if (!this.el) return;
1047
+ this.el.className = `kibee-highlight kibee-highlight--${variant}`;
1048
+ this.el.style.left = `${rect.left - 6}px`;
1049
+ this.el.style.top = `${rect.top - 6}px`;
1050
+ this.el.style.width = `${rect.width + 12}px`;
1051
+ this.el.style.height = `${rect.height + 12}px`;
1052
+ }
1053
+ hide() {
1054
+ if (!this.el) return;
1055
+ this.el.className = "kibee-highlight kibee-highlight--hidden";
1056
+ }
1057
+ destroy() {
1058
+ if (!this.el) return;
1059
+ this.el.remove();
1060
+ this.el = null;
1061
+ }
1062
+ };
1063
+
1064
+ // src/KiBeeClient.ts
1065
+ var VISITOR_ID_KEY = "kibee:visitor_id";
1066
+ function getOrCreateVisitorId() {
1067
+ if (typeof window === "undefined") return `srv_${Date.now()}`;
1068
+ try {
1069
+ const existing = localStorage.getItem(VISITOR_ID_KEY);
1070
+ if (existing) return existing;
1071
+ const id = typeof crypto !== "undefined" && crypto.randomUUID ? `kv_${crypto.randomUUID()}` : `kv_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1072
+ localStorage.setItem(VISITOR_ID_KEY, id);
1073
+ return id;
1074
+ } catch {
1075
+ return `kv_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1076
+ }
1077
+ }
1078
+ var KiBeeClient = class {
1079
+ constructor(options) {
1080
+ this.options = options;
1081
+ this.visitorId = options.visitorId ?? getOrCreateVisitorId();
1082
+ this.mountContainer = options.mountContainer;
1083
+ const websiteId = options.websiteId;
1084
+ let publicKey = options.publicKey;
1085
+ if (!publicKey && options.apiKey) {
1086
+ publicKey = options.apiKey;
1087
+ if (typeof console !== "undefined") {
1088
+ console.warn(
1089
+ "[KiBee] `apiKey` option is deprecated \u2014 use `publicKey` instead. Removed in v1.0."
1090
+ );
1091
+ }
1092
+ }
1093
+ const isProd = typeof process !== "undefined" && typeof process.env !== "undefined" && process.env.NODE_ENV === "production";
1094
+ if (isProd && (!websiteId || !publicKey)) {
1095
+ throw new Error(
1096
+ "[KiBee] Missing websiteId or publicKey. Get yours from your KiBee admin dashboard \u2192 Settings \u2192 Setup & Integration."
1097
+ );
1098
+ }
1099
+ if (!isProd && (!websiteId || !publicKey) && typeof console !== "undefined") {
1100
+ console.warn(
1101
+ "[KiBee] Running in dev mode without websiteId \u2014 set websiteId and publicKey for production."
1102
+ );
1103
+ }
1104
+ this.api = new KiBeeApiClient(
1105
+ options.apiBaseUrl ?? "https://api.kibee.ai",
1106
+ {
1107
+ websiteId,
1108
+ publicKey
1109
+ }
1110
+ );
1111
+ this.controller = new FlowController(
1112
+ this.options.renderer,
1113
+ this.locator,
1114
+ this.highlight,
1115
+ this.bookmarks,
1116
+ (command, phase) => {
1117
+ void this.track("command_transition", {
1118
+ commandType: command.type,
1119
+ phase
1120
+ });
1121
+ }
1122
+ );
1123
+ if (options.useBridge && options.apiToken) {
1124
+ const apiToken = options.apiToken;
1125
+ const apiBase = options.apiBaseUrl ?? "";
1126
+ const bridgeVisitorId = this.visitorId;
1127
+ const bridgeUrl = apiBase.replace(/^http/, "ws") + "/v1/bridge";
1128
+ this.bridgeClient = new BridgeClient({
1129
+ bridgeUrl,
1130
+ source: "web",
1131
+ visitorId: bridgeVisitorId,
1132
+ tokenFetcher: async () => {
1133
+ const res = await fetch(`${apiBase}/v1/bridge/token`, {
1134
+ method: "POST",
1135
+ headers: { authorization: `Bearer ${apiToken}` }
1136
+ });
1137
+ if (!res.ok) {
1138
+ throw new Error(`bridge token fetch failed: ${res.status}`);
1139
+ }
1140
+ return res.json();
1141
+ }
1142
+ });
1143
+ if (typeof window !== "undefined") {
1144
+ this.bridgeUnsubscribes.push(
1145
+ this.bridgeClient.on("presence:hello", (env) => {
1146
+ window.dispatchEvent(
1147
+ new CustomEvent("kibee-bridge-presence", {
1148
+ detail: { source: env.source, present: true }
1149
+ })
1150
+ );
1151
+ })
1152
+ );
1153
+ this.bridgeUnsubscribes.push(
1154
+ this.bridgeClient.on("presence:bye", (env) => {
1155
+ window.dispatchEvent(
1156
+ new CustomEvent("kibee-bridge-presence", {
1157
+ detail: { source: env.source, present: false }
1158
+ })
1159
+ );
1160
+ })
1161
+ );
1162
+ }
1163
+ this.bridgeUnsubscribes.push(
1164
+ this.bridgeClient.on("flow:handoff", (envelope) => {
1165
+ const payload = envelope.payload;
1166
+ const flowId = payload != null && typeof payload === "object" && "flowId" in payload && typeof payload.flowId === "string" ? payload.flowId : void 0;
1167
+ if (!flowId || flowId.length === 0) return;
1168
+ void this.runFlowFromHandoff(flowId).catch((err) => {
1169
+ console.warn("[kibee-sdk] flow:handoff run failed", err);
1170
+ });
1171
+ })
1172
+ );
1173
+ this.bridgeUnsubscribes.push(
1174
+ this.bridgeClient.on("voice:state", (envelope) => {
1175
+ const payload = envelope.payload;
1176
+ const stateStr = payload != null && typeof payload === "object" && "state" in payload && typeof payload.state === "string" ? payload.state : void 0;
1177
+ if (typeof stateStr !== "string") return;
1178
+ if (envelope.source === "web") return;
1179
+ this.otherSurfaceListening = stateStr === "listening";
1180
+ if (typeof window !== "undefined") {
1181
+ window.dispatchEvent(
1182
+ new CustomEvent("kibee-voice-state", {
1183
+ detail: { source: envelope.source, state: stateStr }
1184
+ })
1185
+ );
1186
+ }
1187
+ })
1188
+ );
1189
+ }
1190
+ }
1191
+ options;
1192
+ api;
1193
+ locator = new Locator();
1194
+ highlight = new HighlightRenderer();
1195
+ bookmarks = new BookmarkMemory();
1196
+ registry = new TargetRegistry();
1197
+ controller;
1198
+ initPromise = null;
1199
+ mountContainer;
1200
+ currentPageId = "landing";
1201
+ currentSession = null;
1202
+ currentFlowId;
1203
+ adminSyncTimer = null;
1204
+ /** Polling cadence when SSE is unavailable / closed — the visitor
1205
+ * experience depends on this for new admin messages, so it stays
1206
+ * tight (5s default; afforestation passes 2.5s). */
1207
+ fastPollInterval = 5e3;
1208
+ /** Cadence when SSE is healthy. Polling becomes a heartbeat — it only
1209
+ * needs to catch state SSE doesn't push (friction score, pending
1210
+ * pushes) and recover any messages we somehow missed. */
1211
+ slowPollInterval = 3e4;
1212
+ presenceSessionPromise = null;
1213
+ /** EventSource subscription for the per-session visitor stream. */
1214
+ visitorStream = null;
1215
+ /** Last-seen message timestamp for the polling fallback `?since=`. */
1216
+ lastVisitorMessageAt = null;
1217
+ /** Set of message ids the SDK has emitted to the panel — dedupes when a
1218
+ * message arrives via SSE *and* a subsequent poll. */
1219
+ seenMessageIds = /* @__PURE__ */ new Set();
1220
+ /** Distress signals tracker. Reset on a high-confidence resolve so the
1221
+ * bee doesn't nag if the visitor recovers on their own. */
1222
+ consecutiveLowConfidence = 0;
1223
+ lastDistressPromptAt = 0;
1224
+ /** Current chat mode mirrored from the server. The panel reads this to
1225
+ * hide the "talk to a human" chip when an agent is on. */
1226
+ chatMode = "ai";
1227
+ /** Mirror of whether the host's chat panel is currently visible. The
1228
+ * panel dispatches kibee-panel-open / -close — we listen so we can
1229
+ * decide whether to surface an inbound admin message as a bee tooltip
1230
+ * (panel closed → speak it so the visitor notices) or stay quiet
1231
+ * (panel open → the thread already shows it; tooltip would dupe). */
1232
+ panelOpen = false;
1233
+ /** S3 T5: optional cross-surface bridge client. Initialized only when
1234
+ * `useBridge: true` AND `apiToken` are passed at construction. */
1235
+ bridgeClient;
1236
+ /** S4 T4: unsubscribes for handlers registered against `bridgeClient`. Drained
1237
+ * in `destroy()` so we don't leave dangling handlers if a host swaps clients. */
1238
+ bridgeUnsubscribes = [];
1239
+ /** S4 T4: in-flight `flow:handoff` flowIds. Guards against the same flow being
1240
+ * started twice if duplicate envelopes arrive (e.g. retried publish). */
1241
+ inflightHandoffFlowIds = /* @__PURE__ */ new Set();
1242
+ /** S4 T7: mirrors whether another surface (extension/desktop) is currently
1243
+ * holding the listening floor (announced via `voice:state` over the bridge).
1244
+ * The SDK has no built-in voice listening UI today — voice is host-implemented
1245
+ * — so this is exposed as a flag + a `kibee-voice-state` CustomEvent that hosts
1246
+ * can subscribe to in order to suppress their own listening indicator. */
1247
+ otherSurfaceListening = false;
1248
+ visitorId;
1249
+ /**
1250
+ * Connects to the cross-surface bridge (`/v1/bridge`). No-op when
1251
+ * `useBridge` was false or `apiToken` was missing at construction.
1252
+ */
1253
+ async connectBridge() {
1254
+ if (!this.bridgeClient) return;
1255
+ await this.bridgeClient.connect();
1256
+ }
1257
+ /**
1258
+ * Register a handler for inbound bridge envelopes of a specific type.
1259
+ * Returns an unsubscribe function. No-op (returns a noop unsubscribe)
1260
+ * when the bridge isn't initialized.
1261
+ */
1262
+ onBridgeEvent(type, handler) {
1263
+ if (!this.bridgeClient) {
1264
+ return () => {
1265
+ };
1266
+ }
1267
+ return this.bridgeClient.on(type, handler);
1268
+ }
1269
+ /**
1270
+ * S4 T6: ask another surface (typically `desktop`) to point its on-screen
1271
+ * bee at a global screen coordinate. No-op when the bridge isn't wired
1272
+ * (`useBridge: false` or `apiToken` missing) — the caller doesn't need
1273
+ * to feature-detect.
1274
+ *
1275
+ * `tenantId`, `workspaceId`, and `visitorId` set here are advisory: the
1276
+ * broker overwrites identity fields with JWT-derived values on receipt
1277
+ * (S2 T2 invariant). `source: "web"` is hardcoded because the SDK ships
1278
+ * web-only by default; the broker also enforces source against the
1279
+ * value bound at WS handshake.
1280
+ */
1281
+ async requestPointTo(coord) {
1282
+ if (!this.bridgeClient) return;
1283
+ const envelope = {
1284
+ v: 1,
1285
+ source: "web",
1286
+ target: "desktop",
1287
+ tenantId: null,
1288
+ workspaceId: null,
1289
+ visitorId: this.visitorId,
1290
+ sessionId: this.currentSession?.id ?? null,
1291
+ type: "pointer:show",
1292
+ ts: Date.now(),
1293
+ payload: coord
1294
+ };
1295
+ this.bridgeClient.send(envelope);
1296
+ }
1297
+ async init() {
1298
+ if (this.initPromise) return this.initPromise;
1299
+ if (typeof window !== "undefined") {
1300
+ window.addEventListener("kibee-panel-open", () => {
1301
+ this.panelOpen = true;
1302
+ });
1303
+ window.addEventListener("kibee-panel-close", () => {
1304
+ this.panelOpen = false;
1305
+ });
1306
+ }
1307
+ this.initPromise = (async () => {
1308
+ await this.options.renderer.mount(this.mountContainer);
1309
+ await this.controller.init();
1310
+ this.options.renderer.showActions((choice) => {
1311
+ const kind = choice === "honey" ? "honey" : "sting";
1312
+ const baseLabel = choice === "honey" ? "Bookmarked" : "Friction point";
1313
+ const beePos = this.options.renderer.getBeePosition?.() ?? { x: 0, y: 0 };
1314
+ const context = captureContext({ beeX: beePos.x, beeY: beePos.y });
1315
+ const promptMessage = choice === "honey" ? "What's worth coming back to?" : "What felt rough? (one line is plenty)";
1316
+ const placeholder = choice === "honey" ? 'e.g. "Pricing examples table"' : 'e.g. "Filter resets on back-nav"';
1317
+ void this.options.renderer.prompt(promptMessage, { placeholder }).then((userNote) => {
1318
+ const note = userNote && userNote.length > 0 ? userNote : choice === "honey" ? "User bookmarked this page" : "User flagged friction on this page";
1319
+ const nearText = context.nearestElement?.text;
1320
+ const label = nearText ? `${baseLabel}: ${nearText}` : baseLabel;
1321
+ const bookmark = this.bookmarks.create({
1322
+ kind,
1323
+ label,
1324
+ target: { kibeeId: this.currentPageId },
1325
+ note,
1326
+ metadata: {
1327
+ userNoteProvided: typeof userNote === "string" && userNote.length > 0,
1328
+ context
1329
+ }
1330
+ });
1331
+ if (bookmark) {
1332
+ this.options.renderer.speak(
1333
+ choice === "honey" ? "Saved! This page is bookmarked." : "Noted! This friction point has been recorded."
1334
+ );
1335
+ void this.track("bookmark_saved", {
1336
+ kind,
1337
+ label,
1338
+ hasUserNote: typeof userNote === "string" && userNote.length > 0,
1339
+ pathname: context.pathname,
1340
+ nearestTag: context.nearestElement?.tag ?? null,
1341
+ nearestKibeeId: context.nearestElement?.kibeeId ?? null
1342
+ });
1343
+ }
1344
+ });
1345
+ });
1346
+ await this.track("runtime_initialized");
1347
+ })();
1348
+ return this.initPromise;
1349
+ }
1350
+ identifyPage(pageId, metadata) {
1351
+ this.currentPageId = pageId;
1352
+ void this.track("page_identified", metadata);
1353
+ void this.ensurePresenceSession();
1354
+ }
1355
+ registerTargets(targets) {
1356
+ this.registry.set(targets);
1357
+ void this.track("targets_registered", {
1358
+ count: targets.length,
1359
+ targetIds: targets.map((target) => target.id)
1360
+ });
1361
+ }
1362
+ getRegisteredTargets() {
1363
+ return this.registry.list();
1364
+ }
1365
+ async runFlow(flowOrId) {
1366
+ await this.init();
1367
+ let flow;
1368
+ try {
1369
+ flow = typeof flowOrId === "string" ? await this.api.fetchFlow(flowOrId) : flowOrId;
1370
+ } catch {
1371
+ this.options.renderer.setState("thinking");
1372
+ this.options.renderer.speak(
1373
+ "KiBee can't reach the orchestration API right now. Start the API service to run guided flows."
1374
+ );
1375
+ throw new Error("Unable to fetch KiBee flow definition.");
1376
+ }
1377
+ this.currentFlowId = flow.id;
1378
+ this.currentSession = await this.startSession(flow.id);
1379
+ await this.track("flow_started", {
1380
+ flowId: flow.id,
1381
+ title: flow.title,
1382
+ goal: flow.goal
1383
+ });
1384
+ await this.controller.runFlow(flow);
1385
+ this.currentSession = this.currentSession ? {
1386
+ ...this.currentSession,
1387
+ status: "completed",
1388
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1389
+ } : null;
1390
+ await this.track("flow_completed", {
1391
+ flowId: flow.id,
1392
+ title: flow.title
1393
+ });
1394
+ return flow;
1395
+ }
1396
+ /**
1397
+ * Run a flow in interactive step-through mode: the bee tooltip shows
1398
+ * prev / next buttons so the user controls the pace instead of
1399
+ * auto-advancing on click/input events.
1400
+ */
1401
+ async runInteractiveFlow(flowOrId) {
1402
+ await this.init();
1403
+ let flow;
1404
+ try {
1405
+ flow = typeof flowOrId === "string" ? await this.api.fetchFlow(flowOrId) : flowOrId;
1406
+ } catch {
1407
+ this.options.renderer.setState("thinking");
1408
+ this.options.renderer.speak(
1409
+ "KiBee can't reach the orchestration API right now. Start the API service to run guided flows."
1410
+ );
1411
+ throw new Error("Unable to fetch KiBee flow definition.");
1412
+ }
1413
+ this.currentFlowId = flow.id;
1414
+ this.currentSession = await this.startSession(flow.id);
1415
+ await this.track("flow_started", {
1416
+ flowId: flow.id,
1417
+ title: flow.title,
1418
+ goal: flow.goal,
1419
+ mode: "interactive"
1420
+ });
1421
+ await this.controller.runInteractiveFlow(flow);
1422
+ return flow;
1423
+ }
1424
+ async ask(input) {
1425
+ await this.init();
1426
+ this.options.renderer.setState("thinking");
1427
+ let match;
1428
+ try {
1429
+ match = await this.api.resolveIntent({
1430
+ input,
1431
+ pageId: this.currentPageId,
1432
+ visibleTargets: this.registry.getVisibleTargets(),
1433
+ sessionId: this.currentSession?.id
1434
+ });
1435
+ } catch {
1436
+ this.options.renderer.speak(
1437
+ "KiBee can't resolve intent while the API is offline."
1438
+ );
1439
+ throw new Error("Unable to resolve KiBee intent.");
1440
+ }
1441
+ if (match.message) {
1442
+ this.options.renderer.speak(match.message);
1443
+ }
1444
+ await this.track("intent_resolved", {
1445
+ input,
1446
+ flowId: match.flowId ?? void 0,
1447
+ confidence: match.confidence
1448
+ });
1449
+ if (match.flowId) {
1450
+ await this.runFlow(match.flowId);
1451
+ }
1452
+ return match;
1453
+ }
1454
+ /**
1455
+ * Visitor-facing chat send. Wraps `ask()` so the rule-matched answer
1456
+ * still drives flows when intent is clear, but ALSO:
1457
+ * - emits a `kibee-message-received` event for each customer + ai
1458
+ * message so the panel renders them as bubbles even before the SSE
1459
+ * stream catches up
1460
+ * - tracks distress signals (low confidence streaks, "human"
1461
+ * keywords) and dispatches `kibee-distress-detected` so the panel
1462
+ * can surface the inline "want a teammate?" prompt
1463
+ */
1464
+ async send(input) {
1465
+ if (!input.trim()) return;
1466
+ if (typeof window !== "undefined") {
1467
+ window.dispatchEvent(
1468
+ new CustomEvent("kibee-message-received", {
1469
+ detail: {
1470
+ id: `local-${Date.now()}`,
1471
+ sender: "customer",
1472
+ text: input,
1473
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1474
+ optimistic: true
1475
+ }
1476
+ })
1477
+ );
1478
+ }
1479
+ if (/\b(human|agent|support|talk to (someone|person))\b/i.test(input)) {
1480
+ void this.requestHumanFromKeyword();
1481
+ }
1482
+ const match = await this.ask(input);
1483
+ if (match.confidence < 0.35) {
1484
+ this.consecutiveLowConfidence += 1;
1485
+ if (this.consecutiveLowConfidence >= 2 && this.shouldShowDistressPrompt()) {
1486
+ this.dispatchDistressPrompt("low_confidence_streak");
1487
+ }
1488
+ } else if (match.confidence >= 0.55) {
1489
+ this.consecutiveLowConfidence = 0;
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Visitor-initiated escalation. Called by the panel's "Talk to a human"
1494
+ * chip and by the inline distress prompt's "Yes please" button.
1495
+ */
1496
+ async escalateToHuman() {
1497
+ if (!this.currentSession) return;
1498
+ try {
1499
+ await this.api.escalateToHuman(this.currentSession.id);
1500
+ if (typeof window !== "undefined") {
1501
+ window.dispatchEvent(
1502
+ new CustomEvent("kibee-escalated", { detail: { sessionId: this.currentSession.id } })
1503
+ );
1504
+ }
1505
+ } catch {
1506
+ }
1507
+ }
1508
+ /**
1509
+ * Surface an incoming agent / AI message as a bee speech bubble when the
1510
+ * panel is closed, so a passive visitor sees that someone replied. The
1511
+ * panel-open path stays quiet because the panel thread already shows the
1512
+ * message — speaking would dupe it.
1513
+ *
1514
+ * Skipped for `customer` (the visitor's own echoed message) and `system`
1515
+ * (admin annotations like "Sahil joined the conversation" — visible only
1516
+ * inside the panel).
1517
+ */
1518
+ maybeSpeakIncomingMessage(msg) {
1519
+ if (this.panelOpen) return;
1520
+ if (msg.sender !== "agent" && msg.sender !== "ai") return;
1521
+ if (!msg.text) return;
1522
+ try {
1523
+ this.options.renderer.speak(msg.text, { maxWidth: 280 });
1524
+ } catch {
1525
+ }
1526
+ }
1527
+ shouldShowDistressPrompt() {
1528
+ if (this.chatMode === "human") return false;
1529
+ return Date.now() - this.lastDistressPromptAt > 9e4;
1530
+ }
1531
+ dispatchDistressPrompt(reason) {
1532
+ this.lastDistressPromptAt = Date.now();
1533
+ if (typeof window === "undefined") return;
1534
+ window.dispatchEvent(
1535
+ new CustomEvent("kibee-distress-detected", { detail: { reason } })
1536
+ );
1537
+ }
1538
+ async requestHumanFromKeyword() {
1539
+ if (this.chatMode === "human") return;
1540
+ if (this.shouldShowDistressPrompt()) {
1541
+ this.dispatchDistressPrompt("keyword");
1542
+ }
1543
+ }
1544
+ async saveHoney(label, target, note) {
1545
+ await this.init();
1546
+ const bookmark = await this.controller.createHoneyBookmark(label, target, note);
1547
+ if (bookmark) {
1548
+ await this.track("bookmark_saved", {
1549
+ kind: "honey",
1550
+ label
1551
+ });
1552
+ }
1553
+ return bookmark;
1554
+ }
1555
+ async saveSting(label, target, note) {
1556
+ await this.init();
1557
+ const bookmark = await this.controller.createStingBookmark(label, target, note);
1558
+ if (bookmark) {
1559
+ await this.track("bookmark_saved", {
1560
+ kind: "sting",
1561
+ label
1562
+ });
1563
+ }
1564
+ return bookmark;
1565
+ }
1566
+ listBookmarks() {
1567
+ return this.controller.listBookmarks();
1568
+ }
1569
+ removeBookmark(id) {
1570
+ this.controller.removeBookmark(id);
1571
+ }
1572
+ async revisitBookmark(bookmark) {
1573
+ await this.controller.revisitBookmark(bookmark);
1574
+ }
1575
+ clearFocus() {
1576
+ this.controller.clearFocus();
1577
+ }
1578
+ /**
1579
+ * When true, the dock button toggles the in-app assistant (kibee-assist-toggle)
1580
+ * instead of immediately undocking the bee.
1581
+ */
1582
+ setDockAssistPanel(enabled) {
1583
+ this.options.renderer.setDockClickBehavior(
1584
+ enabled ? "assist-panel" : "undock"
1585
+ );
1586
+ }
1587
+ async track(type, metadata) {
1588
+ const event = {
1589
+ type,
1590
+ pageId: this.currentPageId,
1591
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1592
+ visitorId: this.visitorId,
1593
+ sessionId: this.currentSession?.id,
1594
+ flowId: this.currentFlowId,
1595
+ metadata
1596
+ };
1597
+ this.options.onEvent?.(event);
1598
+ try {
1599
+ await this.api.trackEvent({ event });
1600
+ } catch {
1601
+ }
1602
+ }
1603
+ /** Returns the dock's current screen position (center of the dock button) */
1604
+ getDockPosition() {
1605
+ return this.options.renderer.getDockScreenPosition();
1606
+ }
1607
+ /**
1608
+ * Polls the API for admin-pushed flows/targets AND drains new chat
1609
+ * messages on the same tick. Also opens the per-session visitor SSE
1610
+ * stream so messages arrive in real time when the network supports it
1611
+ * (polling stays as a guaranteed-delivery backstop).
1612
+ *
1613
+ * Call after init() and startSession() to enable admin push support.
1614
+ *
1615
+ * Cadence is SSE-aware: when the visitor stream is OPEN (realtime is
1616
+ * delivering messages), polling backs off to a 30s heartbeat — just
1617
+ * enough to catch state the SSE channel can't push (e.g. friction
1618
+ * score, drained pending pushes). On SSE error/close the interval
1619
+ * snaps back to `intervalMs` so polling carries the load until SSE
1620
+ * reconnects.
1621
+ *
1622
+ * The single `/v1/visitor/sync` endpoint returns messages + pending
1623
+ * pushes + mode + friction in one round-trip, so each tick is one
1624
+ * request instead of two.
1625
+ */
1626
+ enableAdminSync(intervalMs = 5e3) {
1627
+ this.disableAdminSync();
1628
+ this.fastPollInterval = intervalMs;
1629
+ void this.pollVisitorSync();
1630
+ this.armSyncTimer();
1631
+ this.openVisitorStream();
1632
+ }
1633
+ disableAdminSync() {
1634
+ if (this.adminSyncTimer) {
1635
+ clearInterval(this.adminSyncTimer);
1636
+ this.adminSyncTimer = null;
1637
+ }
1638
+ if (this.visitorStream) {
1639
+ this.visitorStream.close();
1640
+ this.visitorStream = null;
1641
+ }
1642
+ }
1643
+ /** (Re)arm the polling interval based on current SSE health. Called
1644
+ * whenever SSE state changes (open / error) so the interval reflects
1645
+ * reality without dropping a tick. */
1646
+ armSyncTimer() {
1647
+ if (this.adminSyncTimer) {
1648
+ clearInterval(this.adminSyncTimer);
1649
+ this.adminSyncTimer = null;
1650
+ }
1651
+ const healthy = this.visitorStream?.readyState === 1;
1652
+ const ms = healthy ? this.slowPollInterval : this.fastPollInterval;
1653
+ this.adminSyncTimer = setInterval(() => {
1654
+ void this.pollVisitorSync();
1655
+ }, ms);
1656
+ }
1657
+ /**
1658
+ * Single visitor poll. Drains pending flow pushes (admin → SDK) AND
1659
+ * sucks in new chat messages in one HTTP request. Always runs alongside
1660
+ * SSE so a missed event during network blips eventually shows up;
1661
+ * `seenMessageIds` dedupes anything SSE already delivered.
1662
+ */
1663
+ async pollVisitorSync() {
1664
+ if (!this.currentSession) return;
1665
+ try {
1666
+ const data = await this.api.fetchVisitorSync(
1667
+ this.currentSession.id,
1668
+ this.lastVisitorMessageAt
1669
+ );
1670
+ this.handleVisitorPoll(data);
1671
+ for (const push of data.pendingPushes) {
1672
+ if (push.flowId) {
1673
+ const flow = await this.api.fetchFlow(push.flowId);
1674
+ if (flow) await this.runFlow(flow);
1675
+ }
1676
+ }
1677
+ } catch {
1678
+ }
1679
+ }
1680
+ handleVisitorPoll(data) {
1681
+ if (typeof window === "undefined") return;
1682
+ if (data.mode !== this.chatMode) {
1683
+ this.chatMode = data.mode;
1684
+ window.dispatchEvent(
1685
+ new CustomEvent("kibee-mode-changed", {
1686
+ detail: { mode: data.mode, hasAgent: data.hasAgent }
1687
+ })
1688
+ );
1689
+ }
1690
+ for (const msg of data.messages) {
1691
+ if (this.seenMessageIds.has(msg.id)) continue;
1692
+ this.seenMessageIds.add(msg.id);
1693
+ window.dispatchEvent(new CustomEvent("kibee-message-received", { detail: msg }));
1694
+ this.maybeSpeakIncomingMessage(msg);
1695
+ }
1696
+ if (data.lastMessageAt) {
1697
+ this.lastVisitorMessageAt = data.lastMessageAt;
1698
+ }
1699
+ if (data.frictionScore > 120 && this.shouldShowDistressPrompt()) {
1700
+ window.dispatchEvent(
1701
+ new CustomEvent("kibee-friction-high", {
1702
+ detail: { score: data.frictionScore, pageId: this.currentPageId }
1703
+ })
1704
+ );
1705
+ }
1706
+ }
1707
+ openVisitorStream() {
1708
+ if (!this.currentSession || this.visitorStream) return;
1709
+ const stream = this.api.openVisitorStream(this.currentSession.id);
1710
+ if (!stream) return;
1711
+ stream.onopen = () => {
1712
+ this.armSyncTimer();
1713
+ };
1714
+ stream.onmessage = (ev) => {
1715
+ try {
1716
+ const event = JSON.parse(ev.data);
1717
+ if (event.type === "message_sent") {
1718
+ const msg = event.payload;
1719
+ if (msg && msg.id && !this.seenMessageIds.has(msg.id)) {
1720
+ this.seenMessageIds.add(msg.id);
1721
+ this.lastVisitorMessageAt = msg.timestamp;
1722
+ if (typeof window !== "undefined") {
1723
+ window.dispatchEvent(new CustomEvent("kibee-message-received", { detail: msg }));
1724
+ }
1725
+ this.maybeSpeakIncomingMessage(msg);
1726
+ }
1727
+ } else if (event.type === "session_mode_changed") {
1728
+ const payload = event.payload;
1729
+ if (payload.mode !== this.chatMode) {
1730
+ this.chatMode = payload.mode;
1731
+ if (typeof window !== "undefined") {
1732
+ window.dispatchEvent(
1733
+ new CustomEvent("kibee-mode-changed", { detail: payload })
1734
+ );
1735
+ }
1736
+ }
1737
+ }
1738
+ } catch {
1739
+ }
1740
+ };
1741
+ stream.onerror = () => {
1742
+ if (stream.readyState === 2) {
1743
+ stream.close();
1744
+ this.visitorStream = null;
1745
+ }
1746
+ this.armSyncTimer();
1747
+ };
1748
+ this.visitorStream = stream;
1749
+ }
1750
+ destroy() {
1751
+ for (const unsubscribe of this.bridgeUnsubscribes) {
1752
+ try {
1753
+ unsubscribe();
1754
+ } catch {
1755
+ }
1756
+ }
1757
+ this.bridgeUnsubscribes = [];
1758
+ this.inflightHandoffFlowIds.clear();
1759
+ this.bridgeClient?.close();
1760
+ this.bridgeClient = void 0;
1761
+ this.disableAdminSync();
1762
+ this.options.renderer.destroy();
1763
+ this.clearFocus();
1764
+ }
1765
+ /**
1766
+ * S4 T4: run a flow handed off via the bridge. Wraps `runFlow(flowId)` with
1767
+ * an in-flight guard so duplicate `flow:handoff` envelopes for the same
1768
+ * flowId (e.g. retried publish) don't start two concurrent runs.
1769
+ */
1770
+ async runFlowFromHandoff(flowId) {
1771
+ if (this.inflightHandoffFlowIds.has(flowId)) return;
1772
+ if (!this.bridgeClient) return;
1773
+ this.inflightHandoffFlowIds.add(flowId);
1774
+ try {
1775
+ await this.runFlow(flowId);
1776
+ } finally {
1777
+ this.inflightHandoffFlowIds.delete(flowId);
1778
+ }
1779
+ }
1780
+ async startSession(flowId) {
1781
+ try {
1782
+ return await this.api.startSession({
1783
+ visitorId: this.visitorId,
1784
+ pageId: this.currentPageId,
1785
+ flowId
1786
+ });
1787
+ } catch {
1788
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1789
+ return {
1790
+ id: `local_${Math.random().toString(36).slice(2, 10)}`,
1791
+ visitorId: this.visitorId,
1792
+ pageId: this.currentPageId,
1793
+ flowId,
1794
+ currentStepIndex: 0,
1795
+ status: "active",
1796
+ startedAt: now,
1797
+ updatedAt: now
1798
+ };
1799
+ }
1800
+ }
1801
+ /**
1802
+ * Creates a server session once the page is known so admin "Visitors" and
1803
+ * analytics can attach to a real `sessionId` (not only when a flow runs).
1804
+ */
1805
+ async ensurePresenceSession() {
1806
+ if (this.currentSession?.status === "active") return;
1807
+ if (this.presenceSessionPromise) {
1808
+ await this.presenceSessionPromise;
1809
+ return;
1810
+ }
1811
+ this.presenceSessionPromise = (async () => {
1812
+ await this.init();
1813
+ if (this.currentSession?.status === "active") return;
1814
+ this.currentSession = await this.startSession(void 0);
1815
+ })().finally(() => {
1816
+ this.presenceSessionPromise = null;
1817
+ });
1818
+ await this.presenceSessionPromise;
1819
+ }
1820
+ };
1821
+ // Annotate the CommonJS export names for ESM import in node:
1822
+ 0 && (module.exports = {
1823
+ BookmarkMemory,
1824
+ BridgeClient,
1825
+ FlowController,
1826
+ HighlightRenderer,
1827
+ KiBeeApiClient,
1828
+ KiBeeClient,
1829
+ Locator,
1830
+ TargetRegistry
1831
+ });