@sightspool/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.cjs ADDED
@@ -0,0 +1,813 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __esm = (fn, res) => function __init() {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ };
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+
15
+ // src/privacy.ts
16
+ function redactText(input) {
17
+ if (!input) return "";
18
+ try {
19
+ return String(input).replace(EMAIL_RE, "\u2039email\u203A").replace(LONG_DIGITS_RE, "\u2039num\u203A").replace(/\s+/g, " ").trim().slice(0, MAX_LABEL_LEN);
20
+ } catch (e) {
21
+ return "";
22
+ }
23
+ }
24
+ function safeClosest(el, selector) {
25
+ try {
26
+ return typeof el.closest === "function" && el.closest(selector) != null;
27
+ } catch (e) {
28
+ return false;
29
+ }
30
+ }
31
+ function isIgnoredElement(el, blockSelectors = []) {
32
+ if (!el || typeof el.closest !== "function") return false;
33
+ if (safeClosest(el, `[${IGNORE_ATTR}]`)) return true;
34
+ for (const sel of blockSelectors) {
35
+ if (sel && safeClosest(el, sel)) return true;
36
+ }
37
+ return false;
38
+ }
39
+ function matchesRedactSelector(el, redactSelectors = []) {
40
+ if (!el || typeof el.closest !== "function") return false;
41
+ for (const sel of redactSelectors) {
42
+ if (sel && safeClosest(el, sel)) return true;
43
+ }
44
+ return false;
45
+ }
46
+ var EMAIL_RE, LONG_DIGITS_RE, MAX_LABEL_LEN, ATTACH_DISCLOSURE, REDACTED, IGNORE_ATTR;
47
+ var init_privacy = __esm({
48
+ "src/privacy.ts"() {
49
+ EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
50
+ LONG_DIGITS_RE = /\d{6,}/g;
51
+ MAX_LABEL_LEN = 280;
52
+ ATTACH_DISCLOSURE = "Session and account attached automatically.";
53
+ REDACTED = "\u2039redacted\u203A";
54
+ IGNORE_ATTR = "data-sightspool-ignore";
55
+ }
56
+ });
57
+
58
+ // src/prompt.ts
59
+ var prompt_exports = {};
60
+ __export(prompt_exports, {
61
+ showPrompt: () => showPrompt
62
+ });
63
+ function showPrompt(opts) {
64
+ return new Promise((resolve) => {
65
+ if (typeof document === "undefined") {
66
+ resolve({ answer: "dismissed" });
67
+ return;
68
+ }
69
+ let settled = false;
70
+ const host = document.createElement("div");
71
+ const shadow = host.attachShadow({ mode: "open" });
72
+ function finish(result) {
73
+ if (settled) return;
74
+ settled = true;
75
+ try {
76
+ host.remove();
77
+ } catch (e) {
78
+ }
79
+ resolve(result);
80
+ }
81
+ const style = document.createElement("style");
82
+ style.textContent = STYLE;
83
+ shadow.appendChild(style);
84
+ const wrap = document.createElement("div");
85
+ wrap.className = "wrap";
86
+ shadow.appendChild(wrap);
87
+ function render(node) {
88
+ wrap.innerHTML = "";
89
+ const card = document.createElement("div");
90
+ card.className = "card";
91
+ card.style.position = "relative";
92
+ const x = document.createElement("button");
93
+ x.className = "x";
94
+ x.textContent = "\xD7";
95
+ x.setAttribute("aria-label", "Dismiss");
96
+ x.onclick = () => finish({ answer: "dismissed" });
97
+ card.appendChild(x);
98
+ card.appendChild(node);
99
+ const disc = document.createElement("p");
100
+ disc.className = "disc";
101
+ disc.textContent = ATTACH_DISCLOSURE;
102
+ card.appendChild(disc);
103
+ wrap.appendChild(card);
104
+ }
105
+ function stateA() {
106
+ const node = document.createElement("div");
107
+ const q = document.createElement("p");
108
+ q.className = "q";
109
+ q.textContent = "Were you able to do what you came here to do today?";
110
+ node.appendChild(q);
111
+ const row = document.createElement("div");
112
+ row.className = "row";
113
+ const yes = document.createElement("button");
114
+ yes.className = "primary";
115
+ yes.textContent = "Yes";
116
+ yes.onclick = () => finish({ answer: "yes" });
117
+ const no = document.createElement("button");
118
+ no.textContent = "Not really";
119
+ no.onclick = () => stateB();
120
+ row.append(yes, no);
121
+ node.appendChild(row);
122
+ render(node);
123
+ }
124
+ function stateB() {
125
+ const node = document.createElement("div");
126
+ const q = document.createElement("p");
127
+ q.className = "q";
128
+ q.textContent = "What were you trying to do?";
129
+ node.appendChild(q);
130
+ for (const c of opts.candidates.slice(0, 4)) {
131
+ const b = document.createElement("button");
132
+ b.className = "cand";
133
+ b.textContent = c;
134
+ b.onclick = () => finish({ answer: "not_really", intent: c });
135
+ node.appendChild(b);
136
+ }
137
+ const se = document.createElement("button");
138
+ se.className = "cand";
139
+ se.textContent = "Something else\u2026";
140
+ se.onclick = () => stateText();
141
+ node.appendChild(se);
142
+ render(node);
143
+ }
144
+ function stateText() {
145
+ const node = document.createElement("div");
146
+ const q = document.createElement("p");
147
+ q.className = "q";
148
+ q.textContent = "What were you trying to do?";
149
+ node.appendChild(q);
150
+ const input = document.createElement("input");
151
+ input.type = "text";
152
+ input.placeholder = "In your own words\u2026";
153
+ node.appendChild(input);
154
+ const row = document.createElement("div");
155
+ row.className = "row";
156
+ const submit = document.createElement("button");
157
+ submit.className = "primary";
158
+ submit.textContent = "Send";
159
+ submit.onclick = () => finish({ answer: "not_really", verbatim: input.value.trim() || void 0 });
160
+ row.appendChild(submit);
161
+ node.appendChild(row);
162
+ render(node);
163
+ try {
164
+ input.focus();
165
+ } catch (e) {
166
+ }
167
+ }
168
+ try {
169
+ document.body.appendChild(host);
170
+ stateA();
171
+ } catch (e) {
172
+ finish({ answer: "dismissed" });
173
+ }
174
+ });
175
+ }
176
+ var STYLE;
177
+ var init_prompt = __esm({
178
+ "src/prompt.ts"() {
179
+ init_privacy();
180
+ STYLE = `
181
+ :host { all: initial; }
182
+ .wrap { position: fixed; bottom: 20px; right: 20px; z-index: 2147483000;
183
+ width: 320px; max-width: calc(100vw - 32px); font-family: system-ui, -apple-system, sans-serif; }
184
+ .card { background: #fff; color: #18181b; border: 1px solid #e4e4e7; border-radius: 12px;
185
+ box-shadow: 0 10px 30px rgba(0,0,0,.12); padding: 16px; }
186
+ .q { font-size: 14px; font-weight: 600; margin: 0 0 12px; line-height: 1.35; }
187
+ .row { display: flex; gap: 8px; flex-wrap: wrap; }
188
+ button { font: inherit; font-size: 13px; cursor: pointer; border-radius: 8px; padding: 8px 12px;
189
+ border: 1px solid #e4e4e7; background: #fafafa; color: #18181b; }
190
+ button:hover { background: #f4f4f5; }
191
+ button.primary { background: #18181b; color: #fff; border-color: #18181b; }
192
+ .cand { display: block; width: 100%; text-align: left; margin-bottom: 6px; }
193
+ .disc { font-size: 11px; color: #71717a; margin: 12px 0 0; }
194
+ .x { position: absolute; top: 8px; right: 10px; border: none; background: none; font-size: 16px;
195
+ color: #a1a1aa; padding: 2px 6px; }
196
+ input { font: inherit; font-size: 13px; width: 100%; box-sizing: border-box; padding: 8px 10px;
197
+ border: 1px solid #e4e4e7; border-radius: 8px; margin-bottom: 8px; }
198
+ `;
199
+ }
200
+ });
201
+
202
+ // src/capture.ts
203
+ init_privacy();
204
+ var TRAIL_MAX = 30;
205
+ var DEAD_CLICK_MS = 700;
206
+ var RAGE_WINDOW_MS = 1e3;
207
+ var RAGE_COUNT = 3;
208
+ function hasDom() {
209
+ return typeof window !== "undefined" && typeof document !== "undefined";
210
+ }
211
+ function createCapture(opts) {
212
+ var _a2, _b;
213
+ const redactSelectors = (_a2 = opts.redact) != null ? _a2 : [];
214
+ const blockSelectors = (_b = opts.block) != null ? _b : [];
215
+ let buf = [];
216
+ let lastError;
217
+ let started = false;
218
+ const subs = [];
219
+ const cleanups = [];
220
+ let recentClicks = [];
221
+ function notify() {
222
+ for (const cb of subs) {
223
+ try {
224
+ cb();
225
+ } catch (e) {
226
+ }
227
+ }
228
+ }
229
+ function push(type, label) {
230
+ try {
231
+ buf.push({ t: Date.now(), type, label: label ? redactText(label) : void 0 });
232
+ if (buf.length > TRAIL_MAX) buf = buf.slice(-TRAIL_MAX);
233
+ if (opts.debug) console.debug("[sightspool] event", type, label);
234
+ notify();
235
+ } catch (e) {
236
+ }
237
+ }
238
+ function currentRoute() {
239
+ try {
240
+ return location.pathname + location.search;
241
+ } catch (e) {
242
+ return "";
243
+ }
244
+ }
245
+ function currentLabel() {
246
+ try {
247
+ return document.title || "";
248
+ } catch (e) {
249
+ return "";
250
+ }
251
+ }
252
+ function onClick(ev) {
253
+ var _a3;
254
+ try {
255
+ const target = ev.target;
256
+ if (isIgnoredElement(target, blockSelectors)) return;
257
+ const text = target && (((_a3 = target.getAttribute) == null ? void 0 : _a3.call(target, "aria-label")) || target.textContent) || "";
258
+ const label = matchesRedactSelector(target, redactSelectors) ? REDACTED : text.trim().slice(0, 60);
259
+ const t = Date.now();
260
+ recentClicks = recentClicks.filter((ts) => t - ts <= RAGE_WINDOW_MS);
261
+ recentClicks.push(t);
262
+ const isRage = recentClicks.length >= RAGE_COUNT;
263
+ push(isRage ? "rage_click" : "click", label);
264
+ const routeBefore = currentRoute();
265
+ let mutated = false;
266
+ let observer = null;
267
+ try {
268
+ observer = new MutationObserver(() => {
269
+ mutated = true;
270
+ });
271
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true });
272
+ } catch (e) {
273
+ }
274
+ window.setTimeout(() => {
275
+ try {
276
+ observer == null ? void 0 : observer.disconnect();
277
+ if (!mutated && currentRoute() === routeBefore && label) {
278
+ push("dead_click", label);
279
+ }
280
+ } catch (e) {
281
+ }
282
+ }, DEAD_CLICK_MS);
283
+ } catch (e) {
284
+ }
285
+ }
286
+ function onError(ev) {
287
+ try {
288
+ lastError = String(ev.message || "error");
289
+ push("error", lastError);
290
+ } catch (e) {
291
+ }
292
+ }
293
+ function onRejection(ev) {
294
+ try {
295
+ const reason = ev.reason;
296
+ lastError = reason instanceof Error ? reason.message : String(reason);
297
+ push("error", lastError);
298
+ } catch (e) {
299
+ }
300
+ }
301
+ function installRouteTracking() {
302
+ try {
303
+ const fire = () => push("route", currentRoute());
304
+ const origPush = history.pushState;
305
+ const origReplace = history.replaceState;
306
+ history.pushState = function(...args) {
307
+ const r = origPush.apply(this, args);
308
+ fire();
309
+ return r;
310
+ };
311
+ history.replaceState = function(...args) {
312
+ const r = origReplace.apply(this, args);
313
+ fire();
314
+ return r;
315
+ };
316
+ window.addEventListener("popstate", fire);
317
+ cleanups.push(() => {
318
+ history.pushState = origPush;
319
+ history.replaceState = origReplace;
320
+ window.removeEventListener("popstate", fire);
321
+ });
322
+ push("route", currentRoute());
323
+ } catch (e) {
324
+ }
325
+ }
326
+ function installFetchTracking() {
327
+ try {
328
+ if (typeof window.fetch !== "function") return;
329
+ const orig = window.fetch.bind(window);
330
+ window.fetch = async (...args) => {
331
+ try {
332
+ const res = await orig(...args);
333
+ if (!res.ok) push("request_error", `HTTP ${res.status}`);
334
+ return res;
335
+ } catch (err) {
336
+ push("request_error", err instanceof Error ? err.message : "network error");
337
+ throw err;
338
+ }
339
+ };
340
+ cleanups.push(() => {
341
+ window.fetch = orig;
342
+ });
343
+ } catch (e) {
344
+ }
345
+ }
346
+ return {
347
+ start() {
348
+ if (started || !hasDom()) return;
349
+ started = true;
350
+ try {
351
+ document.addEventListener("click", onClick, true);
352
+ window.addEventListener("error", onError);
353
+ window.addEventListener("unhandledrejection", onRejection);
354
+ cleanups.push(() => {
355
+ document.removeEventListener("click", onClick, true);
356
+ window.removeEventListener("error", onError);
357
+ window.removeEventListener("unhandledrejection", onRejection);
358
+ });
359
+ installRouteTracking();
360
+ installFetchTracking();
361
+ } catch (e) {
362
+ }
363
+ },
364
+ stop() {
365
+ started = false;
366
+ for (const c of cleanups.splice(0)) {
367
+ try {
368
+ c();
369
+ } catch (e) {
370
+ }
371
+ }
372
+ },
373
+ trail() {
374
+ return buf.slice();
375
+ },
376
+ route: currentRoute,
377
+ label: currentLabel,
378
+ error() {
379
+ return lastError;
380
+ },
381
+ onEvent(cb) {
382
+ subs.push(cb);
383
+ },
384
+ recordSearch(query, zeroResult) {
385
+ const q = query.trim();
386
+ if (!q) return;
387
+ push(zeroResult ? "zero_result" : "search", q);
388
+ }
389
+ };
390
+ }
391
+
392
+ // src/harvest.ts
393
+ init_privacy();
394
+ var SEARCH_HINT_RE = /search|filter|find|query|command|lookup/i;
395
+ var DEBOUNCE_MS = 800;
396
+ function looksLikeSearch(el) {
397
+ try {
398
+ const input = el;
399
+ if (input.type === "search") return true;
400
+ const role = el.getAttribute("role") || "";
401
+ if (role === "searchbox" || role === "combobox") return true;
402
+ const hay = [
403
+ input.name,
404
+ input.placeholder,
405
+ el.getAttribute("aria-label"),
406
+ el.id,
407
+ el.className
408
+ ].filter(Boolean).join(" ");
409
+ return SEARCH_HINT_RE.test(hay);
410
+ } catch (e) {
411
+ return false;
412
+ }
413
+ }
414
+ function createHarvest(capture, opts) {
415
+ var _a2, _b;
416
+ let hint;
417
+ let timer = null;
418
+ let started = false;
419
+ const redactSelectors = (_a2 = opts.redact) != null ? _a2 : [];
420
+ const blockSelectors = (_b = opts.block) != null ? _b : [];
421
+ function onInput(ev) {
422
+ try {
423
+ const target = ev.target;
424
+ if (!target || target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") return;
425
+ if (!looksLikeSearch(target)) return;
426
+ if (isIgnoredElement(target, blockSelectors) || matchesRedactSelector(target, redactSelectors))
427
+ return;
428
+ const value = (target.value || "").trim();
429
+ if (value.length < 2) return;
430
+ if (timer !== null) window.clearTimeout(timer);
431
+ timer = window.setTimeout(() => {
432
+ hint = value;
433
+ capture.recordSearch(value);
434
+ if (opts.debug) console.debug("[sightspool] harvested intent", value);
435
+ }, DEBOUNCE_MS);
436
+ } catch (e) {
437
+ }
438
+ }
439
+ return {
440
+ start() {
441
+ if (started || typeof document === "undefined") return;
442
+ started = true;
443
+ try {
444
+ document.addEventListener("input", onInput, true);
445
+ } catch (e) {
446
+ }
447
+ },
448
+ stop() {
449
+ started = false;
450
+ try {
451
+ document.removeEventListener("input", onInput, true);
452
+ if (timer !== null) window.clearTimeout(timer);
453
+ } catch (e) {
454
+ }
455
+ },
456
+ lastIntentHint() {
457
+ return hint;
458
+ }
459
+ };
460
+ }
461
+
462
+ // src/egress.ts
463
+ var FLUSH_BATCH = 10;
464
+ var FLUSH_INTERVAL_MS = 15e3;
465
+ function createEgress(opts) {
466
+ let queue = [];
467
+ let identity;
468
+ let interval = null;
469
+ let started = false;
470
+ const url = `${opts.endpoint.replace(/\/+$/, "")}/api/sdk/ingest`;
471
+ function send(signals) {
472
+ if (signals.length === 0) return;
473
+ const body = { key: opts.key, identity, signals };
474
+ const json = JSON.stringify(body);
475
+ try {
476
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
477
+ const blob = new Blob([json], { type: "text/plain" });
478
+ const ok = navigator.sendBeacon(url, blob);
479
+ if (ok) {
480
+ if (opts.debug) console.debug("[sightspool] beacon", signals.length);
481
+ return;
482
+ }
483
+ }
484
+ void fetch(url, {
485
+ method: "POST",
486
+ headers: { "Content-Type": "text/plain" },
487
+ body: json,
488
+ keepalive: true,
489
+ mode: "cors",
490
+ credentials: "omit"
491
+ }).catch(() => {
492
+ });
493
+ } catch (e) {
494
+ }
495
+ }
496
+ function onVisibility() {
497
+ try {
498
+ if (document.visibilityState === "hidden") flush();
499
+ } catch (e) {
500
+ }
501
+ }
502
+ function flush() {
503
+ if (queue.length === 0) return;
504
+ const batch = queue;
505
+ queue = [];
506
+ send(batch);
507
+ }
508
+ return {
509
+ start() {
510
+ if (started || typeof window === "undefined") return;
511
+ started = true;
512
+ try {
513
+ document.addEventListener("visibilitychange", onVisibility);
514
+ window.addEventListener("pagehide", flush);
515
+ interval = window.setInterval(flush, FLUSH_INTERVAL_MS);
516
+ } catch (e) {
517
+ }
518
+ },
519
+ stop() {
520
+ started = false;
521
+ try {
522
+ document.removeEventListener("visibilitychange", onVisibility);
523
+ window.removeEventListener("pagehide", flush);
524
+ if (interval !== null) window.clearInterval(interval);
525
+ } catch (e) {
526
+ }
527
+ flush();
528
+ },
529
+ enqueue(signal) {
530
+ queue.push(signal);
531
+ if (queue.length >= FLUSH_BATCH) flush();
532
+ },
533
+ flush,
534
+ setIdentity(next) {
535
+ identity = next;
536
+ }
537
+ };
538
+ }
539
+
540
+ // src/triggers.ts
541
+ var RAGE_CLICK_COUNT = 3;
542
+ var RAGE_WINDOW_MS2 = 1e3;
543
+ var FRICTION_RETRY_COUNT = 3;
544
+ var FRICTION_WINDOW_MS = 8e3;
545
+ function detectFriction(trail, now, windowMs = FRICTION_WINDOW_MS) {
546
+ var _a2;
547
+ const recent = trail.filter((e) => now - e.t <= windowMs);
548
+ if (recent.some((e) => e.type === "error" || e.type === "request_error")) return "error";
549
+ const clicks = recent.filter((e) => e.type === "click" || e.type === "rage_click");
550
+ for (let i = 0; i < clicks.length; i++) {
551
+ const burst = clicks.filter(
552
+ (e) => e.t >= clicks[i].t && e.t - clicks[i].t <= RAGE_WINDOW_MS2
553
+ );
554
+ if (burst.length >= RAGE_CLICK_COUNT) return "rage";
555
+ }
556
+ if (recent.some((e) => e.type === "dead_click")) return "dead_end";
557
+ const byLabel = /* @__PURE__ */ new Map();
558
+ for (const e of clicks) {
559
+ if (!e.label) continue;
560
+ const n = ((_a2 = byLabel.get(e.label)) != null ? _a2 : 0) + 1;
561
+ byLabel.set(e.label, n);
562
+ if (n >= FRICTION_RETRY_COUNT) return "retry";
563
+ }
564
+ return null;
565
+ }
566
+ var DEFAULT_CAPS = {
567
+ maxPromptsPerSession: 1,
568
+ cooldownMs: 5 * 6e4
569
+ };
570
+ function canPrompt(state, now, caps = DEFAULT_CAPS) {
571
+ if (state.promptsShown >= caps.maxPromptsPerSession) return false;
572
+ if (state.lastPromptAt !== null && now - state.lastPromptAt < caps.cooldownMs) return false;
573
+ return true;
574
+ }
575
+
576
+ // src/env.ts
577
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", ""]);
578
+ function isLocalhost(hostname) {
579
+ const h = (hostname != null ? hostname : "").trim().toLowerCase();
580
+ if (LOCAL_HOSTS.has(h)) return true;
581
+ return h.endsWith(".local") || h.endsWith(".localhost");
582
+ }
583
+
584
+ // src/index.ts
585
+ var DEFAULT_ENDPOINT = "https://app.sightspool.com";
586
+ var FRICTION_EMIT_COOLDOWN_MS = 3e4;
587
+ var EVENT_DEBOUNCE_MS = 600;
588
+ function currentHostname() {
589
+ try {
590
+ return typeof location !== "undefined" ? location.hostname : "";
591
+ } catch (e) {
592
+ return "";
593
+ }
594
+ }
595
+ var ctrl = null;
596
+ function uuid() {
597
+ try {
598
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
599
+ return crypto.randomUUID();
600
+ }
601
+ } catch (e) {
602
+ }
603
+ return "s-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
604
+ }
605
+ function deriveCandidates(c) {
606
+ const out = [];
607
+ const hint = c.harvest.lastIntentHint();
608
+ if (hint) out.push(`Searching for \u201C${hint}\u201D`);
609
+ const label = c.capture.label();
610
+ if (label) out.push(`Something on \u201C${label.slice(0, 40)}\u201D`);
611
+ return out;
612
+ }
613
+ function buildSignal(c, trigger, extra = {}) {
614
+ const err = c.capture.error();
615
+ return {
616
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
617
+ route: c.capture.route(),
618
+ client_label: c.capture.label(),
619
+ trail: c.capture.trail(),
620
+ session_ref: c.sessionRef,
621
+ trigger,
622
+ intent_hint: c.harvest.lastIntentHint(),
623
+ error: err,
624
+ outcome: err || trigger === "friction" ? "failed" : "unknown",
625
+ ...extra
626
+ };
627
+ }
628
+ async function maybePrompt(c, trigger) {
629
+ var _a2;
630
+ const now = Date.now();
631
+ if (canPrompt(c.fatigue, now, DEFAULT_CAPS)) {
632
+ c.fatigue.promptsShown += 1;
633
+ c.fatigue.lastPromptAt = now;
634
+ try {
635
+ const { showPrompt: showPrompt2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
636
+ const result = await showPrompt2({ candidates: deriveCandidates(c) });
637
+ c.egress.enqueue(
638
+ buildSignal(c, trigger, {
639
+ answer: result.answer,
640
+ verbatim: result.verbatim,
641
+ intent_hint: (_a2 = result.intent) != null ? _a2 : c.harvest.lastIntentHint(),
642
+ outcome: result.answer === "yes" ? "achieved" : "failed"
643
+ })
644
+ );
645
+ } catch (e) {
646
+ c.egress.enqueue(buildSignal(c, trigger));
647
+ }
648
+ } else {
649
+ c.egress.enqueue(buildSignal(c, trigger));
650
+ }
651
+ }
652
+ function onCaptureEvent() {
653
+ const c = ctrl;
654
+ if (!c || !c.running) return;
655
+ if (c.eventTimer !== null) return;
656
+ c.eventTimer = (typeof window !== "undefined" ? window.setTimeout : setTimeout)(() => {
657
+ c.eventTimer = null;
658
+ try {
659
+ const now = Date.now();
660
+ if (now - c.lastFrictionEmitAt < FRICTION_EMIT_COOLDOWN_MS) return;
661
+ const kind = detectFriction(c.capture.trail(), now);
662
+ if (!kind) return;
663
+ c.lastFrictionEmitAt = now;
664
+ void maybePrompt(c, "friction");
665
+ } catch (e) {
666
+ }
667
+ }, EVENT_DEBOUNCE_MS);
668
+ }
669
+ function onExitIntent(ev) {
670
+ const c = ctrl;
671
+ if (!c || !c.running || !c.config.boundaryAsk) return;
672
+ try {
673
+ if (ev.clientY <= 0) void maybePrompt(c, "boundary");
674
+ } catch (e) {
675
+ }
676
+ }
677
+ function startRuntime(c) {
678
+ if (c.running) return;
679
+ if (c.suppressed) {
680
+ if (c.config.debug)
681
+ console.debug(
682
+ "[sightspool] capture suppressed on localhost (set captureOnLocalhost:true to override)"
683
+ );
684
+ return;
685
+ }
686
+ c.running = true;
687
+ c.capture.start();
688
+ c.harvest.start();
689
+ c.egress.start();
690
+ c.capture.onEvent(onCaptureEvent);
691
+ try {
692
+ document.addEventListener("mouseout", onExitIntent);
693
+ } catch (e) {
694
+ }
695
+ if (c.config.debug) console.debug("[sightspool] started", c.sessionRef);
696
+ }
697
+ function init(config) {
698
+ try {
699
+ if (!config || !config.key) {
700
+ console.warn("[sightspool] init: a `key` is required");
701
+ return;
702
+ }
703
+ if (ctrl) return;
704
+ const endpoint = config.endpoint || DEFAULT_ENDPOINT;
705
+ const privacyOpts = { redact: config.redact, block: config.block };
706
+ const capture = createCapture({ debug: !!config.debug, ...privacyOpts });
707
+ const harvest = createHarvest(capture, { debug: !!config.debug, ...privacyOpts });
708
+ const egress = createEgress({ key: config.key, endpoint, debug: !!config.debug });
709
+ ctrl = {
710
+ config: {
711
+ ...config,
712
+ key: config.key,
713
+ endpoint,
714
+ boundaryAsk: config.boundaryAsk !== false,
715
+ debug: !!config.debug
716
+ },
717
+ capture,
718
+ harvest,
719
+ egress,
720
+ identity: {},
721
+ sessionRef: uuid(),
722
+ fatigue: { promptsShown: 0, lastPromptAt: null },
723
+ lastFrictionEmitAt: 0,
724
+ eventTimer: null,
725
+ running: false,
726
+ suppressed: isLocalhost(currentHostname()) && config.captureOnLocalhost !== true
727
+ };
728
+ if (config.consent !== false) startRuntime(ctrl);
729
+ } catch (err) {
730
+ try {
731
+ console.warn("[sightspool] init failed", err);
732
+ } catch (e) {
733
+ }
734
+ }
735
+ }
736
+ function identify(userId, traits) {
737
+ try {
738
+ if (!ctrl) return;
739
+ ctrl.identity = { userId, account: traits == null ? void 0 : traits.account, plan: traits == null ? void 0 : traits.plan };
740
+ ctrl.egress.setIdentity(ctrl.identity);
741
+ } catch (e) {
742
+ }
743
+ }
744
+ function start() {
745
+ try {
746
+ if (ctrl) startRuntime(ctrl);
747
+ } catch (e) {
748
+ }
749
+ }
750
+ function consent(granted) {
751
+ try {
752
+ if (!ctrl) return;
753
+ if (granted) startRuntime(ctrl);
754
+ else stop();
755
+ } catch (e) {
756
+ }
757
+ }
758
+ function stop() {
759
+ try {
760
+ if (!ctrl) return;
761
+ ctrl.running = false;
762
+ ctrl.capture.stop();
763
+ ctrl.harvest.stop();
764
+ ctrl.egress.stop();
765
+ try {
766
+ document.removeEventListener("mouseout", onExitIntent);
767
+ } catch (e) {
768
+ }
769
+ } catch (e) {
770
+ }
771
+ }
772
+ var api = { init, identify, start, stop, consent };
773
+ var src_default = api;
774
+ var _a;
775
+ try {
776
+ if (typeof document !== "undefined") {
777
+ const el = (_a = document.currentScript) != null ? _a : document.querySelector("script[data-sightspool-key]");
778
+ const key = el == null ? void 0 : el.getAttribute("data-sightspool-key");
779
+ if (key) {
780
+ let endpoint = (el == null ? void 0 : el.getAttribute("data-sightspool-endpoint")) || void 0;
781
+ if (!endpoint && (el == null ? void 0 : el.src)) {
782
+ try {
783
+ endpoint = new URL(el.src).origin;
784
+ } catch (e) {
785
+ }
786
+ }
787
+ const list = (attr) => {
788
+ const raw = el == null ? void 0 : el.getAttribute(attr);
789
+ if (!raw) return void 0;
790
+ const out = raw.split(",").map((s) => s.trim()).filter(Boolean);
791
+ return out.length ? out : void 0;
792
+ };
793
+ init({
794
+ key,
795
+ endpoint,
796
+ redact: list("data-sightspool-redact"),
797
+ block: list("data-sightspool-block"),
798
+ captureOnLocalhost: (el == null ? void 0 : el.hasAttribute("data-sightspool-capture-localhost")) || void 0,
799
+ debug: (el == null ? void 0 : el.hasAttribute("data-sightspool-debug")) || void 0
800
+ });
801
+ }
802
+ }
803
+ } catch (e) {
804
+ }
805
+
806
+ exports.consent = consent;
807
+ exports.default = src_default;
808
+ exports.identify = identify;
809
+ exports.init = init;
810
+ exports.start = start;
811
+ exports.stop = stop;
812
+ //# sourceMappingURL=index.cjs.map
813
+ //# sourceMappingURL=index.cjs.map