@guidelytech/sdk-core 0.1.0 → 0.2.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
@@ -149,8 +149,10 @@ Messages posted to the opener: `guidely:authoring-ready { sessionId, url }`, `gu
149
149
 
150
150
  | Bundle | minified | min+gz |
151
151
  |---|---|---|
152
- | sdk-core + guide-runtime + floating-ui + `@guidelytech/shared/runtime` (everything an app ships) | 115 kB | **35.8 kB** |
153
- | script-tag build `guidely.js` (authoring schemas inlined) | 215 kB | 62 kB |
152
+ | sdk-core + guide-runtime + floating-ui + `@guidelytech/shared/runtime` (everything an app ships) | 127 kB | **39.4 kB** |
153
+ | Scout messenger chunk (`@guidelytech/guide-runtime/messenger`, loaded on demand) | 26 kB | 8.8 kB |
154
+ | script-tag build `guidely.js` (authoring schemas and messenger inlined) | 244 kB | ~71 kB |
154
155
 
155
156
  The SDK imports only the zod-free `@guidelytech/shared/runtime` entry; the Zod schemas are loaded
156
- lazily and only when the page enters authoring mode. Budget: 45 kB min+gz for the npm runtime.
157
+ lazily and only when the page enters authoring mode. Budget: 45 kB min+gz for the npm runtime and
158
+ 15 kB for the messenger chunk.
package/dist/index.cjs CHANGED
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  SDK_VERSION: () => SDK_VERSION,
44
44
  areMutuallyExclusive: () => areMutuallyExclusive,
45
45
  buildContext: () => buildContext,
46
+ captureSnapshot: () => captureSnapshot,
46
47
  createGuidely: () => createGuidely,
47
48
  createStorage: () => createStorage,
48
49
  default: () => index_default,
@@ -55,13 +56,89 @@ __export(index_exports, {
55
56
  module.exports = __toCommonJS(index_exports);
56
57
 
57
58
  // src/client.ts
58
- var import_runtime5 = require("@guidelytech/shared/runtime");
59
+ var import_runtime6 = require("@guidelytech/shared/runtime");
59
60
  var import_guide_runtime2 = require("@guidelytech/guide-runtime");
60
61
  var import_guide_runtime3 = require("@guidelytech/guide-runtime");
61
62
 
62
63
  // src/authoring.ts
63
64
  var import_guide_runtime = require("@guidelytech/guide-runtime");
64
65
 
66
+ // src/snapshot.ts
67
+ var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
68
+ var STRIP = "script, noscript, template, #guidely-root, [data-guidely], meta[http-equiv], link[rel~='preload'], link[rel~='modulepreload'], link[rel~='prefetch'], link[rel~='manifest'], link[rel~='serviceworker']";
69
+ function rulesText(sheet) {
70
+ if (!sheet) return null;
71
+ try {
72
+ return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ function maskText(root) {
78
+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
79
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
80
+ n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
81
+ }
82
+ }
83
+ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
84
+ const win = doc.defaultView;
85
+ if (!win || !doc.documentElement) return null;
86
+ const clone = doc.documentElement.cloneNode(true);
87
+ const origStyles = doc.documentElement.querySelectorAll("style");
88
+ clone.querySelectorAll("style").forEach((el, i) => {
89
+ const css = rulesText(origStyles[i]?.sheet);
90
+ if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
91
+ });
92
+ const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
93
+ clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
94
+ const r = origFrames[i]?.getBoundingClientRect();
95
+ const box = doc.createElement("div");
96
+ box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
97
+ if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
98
+ el.replaceWith(box);
99
+ });
100
+ clone.querySelectorAll(STRIP).forEach((el) => el.remove());
101
+ for (const sel of maskSelectors) {
102
+ try {
103
+ clone.querySelectorAll(sel).forEach(maskText);
104
+ } catch {
105
+ }
106
+ }
107
+ clone.querySelectorAll("*").forEach((el) => {
108
+ for (const attr of Array.from(el.attributes)) {
109
+ const name = attr.name.toLowerCase();
110
+ if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr.value)) el.removeAttribute(attr.name);
111
+ }
112
+ if (el instanceof HTMLInputElement) {
113
+ el.removeAttribute("value");
114
+ el.removeAttribute("checked");
115
+ } else if (el instanceof HTMLTextAreaElement) {
116
+ el.textContent = "";
117
+ }
118
+ el.removeAttribute("autofocus");
119
+ });
120
+ const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
121
+ let head = clone.querySelector("head");
122
+ if (!head) {
123
+ head = doc.createElement("head");
124
+ clone.prepend(head);
125
+ }
126
+ if (adopted) {
127
+ const style = doc.createElement("style");
128
+ style.textContent = adopted;
129
+ head.append(style);
130
+ }
131
+ clone.querySelectorAll("base").forEach((el) => el.remove());
132
+ const base = doc.createElement("base");
133
+ base.setAttribute("href", doc.baseURI || win.location.href);
134
+ base.setAttribute("target", "_blank");
135
+ head.prepend(base);
136
+ const html = `<!DOCTYPE html>
137
+ ${clone.outerHTML}`;
138
+ if (html.length > maxBytes) return null;
139
+ return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
140
+ }
141
+
65
142
  // src/storage.ts
66
143
  function memoryStore() {
67
144
  const map = /* @__PURE__ */ new Map();
@@ -129,7 +206,9 @@ var KEYS = {
129
206
  queue: (key) => `guidely:queue:${key}`,
130
207
  session: "guidely:session",
131
208
  active: "guidely:active",
132
- authoring: "guidely:authoring"
209
+ authoring: "guidely:authoring",
210
+ /** Messenger conversations this browser can read: [{ id, token }]. */
211
+ conversations: (key) => `guidely:conversations:${key}`
133
212
  };
134
213
 
135
214
  // src/authoring.ts
@@ -139,6 +218,8 @@ async function loadSchemas() {
139
218
  }
140
219
  var AUTHORING_PARAM = "guidely_authoring";
141
220
  var HEARTBEAT_MS = 15e3;
221
+ var SNAPSHOT_DEBOUNCE_MS = 800;
222
+ var SNAPSHOT_MAX_WAIT_MS = 4e3;
142
223
  var AuthoringBridge = class {
143
224
  constructor(opts) {
144
225
  __publicField(this, "opts", opts);
@@ -149,6 +230,10 @@ var AuthoringBridge = class {
149
230
  __publicField(this, "preview", null);
150
231
  __publicField(this, "previewGuide", null);
151
232
  __publicField(this, "active", false);
233
+ __publicField(this, "observer", null);
234
+ __publicField(this, "snapshotTimer", null);
235
+ __publicField(this, "snapshotDue", 0);
236
+ __publicField(this, "lastSnapshot", "");
152
237
  }
153
238
  isActive() {
154
239
  return this.active;
@@ -204,6 +289,7 @@ var AuthoringBridge = class {
204
289
  });
205
290
  }, HEARTBEAT_MS);
206
291
  this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
292
+ this.watchPage();
207
293
  } catch (err) {
208
294
  this.opts.onError(err);
209
295
  }
@@ -225,6 +311,32 @@ var AuthoringBridge = class {
225
311
  this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
226
312
  void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
227
313
  }
314
+ /** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
315
+ watchPage() {
316
+ this.scheduleSnapshot();
317
+ if (typeof MutationObserver === "undefined") return;
318
+ this.observer = new MutationObserver(() => this.scheduleSnapshot());
319
+ this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
320
+ }
321
+ scheduleSnapshot() {
322
+ const now = Date.now();
323
+ if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
324
+ else clearTimeout(this.snapshotTimer);
325
+ this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
326
+ }
327
+ sendSnapshot() {
328
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
329
+ this.snapshotTimer = null;
330
+ if (!this.info || !this.active) return;
331
+ try {
332
+ const snapshot = captureSnapshot(document, this.opts.maskSelectors);
333
+ if (!snapshot || snapshot.html === this.lastSnapshot) return;
334
+ this.lastSnapshot = snapshot.html;
335
+ this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
336
+ } catch (err) {
337
+ this.opts.onError(err);
338
+ }
339
+ }
228
340
  post(message) {
229
341
  if (!this.info) return;
230
342
  try {
@@ -292,6 +404,10 @@ var AuthoringBridge = class {
292
404
  case "guidely:stop-preview":
293
405
  this.stopPreview();
294
406
  break;
407
+ case "guidely:request-snapshot":
408
+ this.lastSnapshot = "";
409
+ this.sendSnapshot();
410
+ break;
295
411
  case "guidely:ping":
296
412
  this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
297
413
  break;
@@ -310,6 +426,10 @@ var AuthoringBridge = class {
310
426
  if (!this.active) return;
311
427
  this.active = false;
312
428
  this.stopPreview();
429
+ this.observer?.disconnect();
430
+ this.observer = null;
431
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
432
+ this.snapshotTimer = null;
313
433
  if (this.heartbeat) clearInterval(this.heartbeat);
314
434
  this.heartbeat = null;
315
435
  this.session?.stop();
@@ -478,7 +598,7 @@ var Api = class {
478
598
  async request(method, path, options = {}) {
479
599
  const f = this.fetcher;
480
600
  if (!f) throw new Error("fetch is not available");
481
- const headers = { "X-Guidely-Key": this.key, Accept: "application/json" };
601
+ const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
482
602
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
483
603
  if (options.etag) headers["If-None-Match"] = options.etag;
484
604
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
@@ -512,6 +632,33 @@ var Api = class {
512
632
  post(path, body, options) {
513
633
  return this.request("POST", path, { body, ...options });
514
634
  }
635
+ /**
636
+ * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
637
+ * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
638
+ */
639
+ async stream(path, body, onEvent, headers = {}) {
640
+ const f = this.fetcher;
641
+ if (!f) throw new Error("fetch is not available");
642
+ const res = await f(this.url(path), {
643
+ method: "POST",
644
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
645
+ body: JSON.stringify(body),
646
+ credentials: "omit",
647
+ mode: "cors"
648
+ });
649
+ const type = res.headers?.get?.("content-type") ?? "";
650
+ if (!type.includes("text/event-stream") || !res.body) {
651
+ let data = null;
652
+ try {
653
+ data = await res.json();
654
+ } catch {
655
+ data = null;
656
+ }
657
+ return { status: res.status, ok: res.ok, data, etag: null };
658
+ }
659
+ await readEventStream(res.body, onEvent);
660
+ return { status: res.status, ok: res.ok, data: null, etag: null };
661
+ }
515
662
  /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
516
663
  beacon(path, body) {
517
664
  try {
@@ -538,6 +685,143 @@ async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
538
685
  }
539
686
  return null;
540
687
  }
688
+ async function readEventStream(body, onEvent) {
689
+ const reader = body.getReader();
690
+ const decoder = new TextDecoder();
691
+ let buffer = "";
692
+ const dispatch = (raw) => {
693
+ let event = "message";
694
+ const data = [];
695
+ for (const line of raw.split("\n")) {
696
+ if (!line || line.startsWith(":")) continue;
697
+ const i = line.indexOf(":");
698
+ const field = i < 0 ? line : line.slice(0, i);
699
+ const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
700
+ if (field === "event") event = value;
701
+ else if (field === "data") data.push(value);
702
+ }
703
+ if (!data.length) return;
704
+ try {
705
+ onEvent(event, JSON.parse(data.join("\n")));
706
+ } catch {
707
+ }
708
+ };
709
+ for (; ; ) {
710
+ const { done, value } = await reader.read();
711
+ if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
712
+ let sep;
713
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
714
+ dispatch(buffer.slice(0, sep));
715
+ buffer = buffer.slice(sep + 2);
716
+ }
717
+ if (done) break;
718
+ }
719
+ if (buffer.trim()) dispatch(buffer);
720
+ }
721
+
722
+ // src/messenger-api.ts
723
+ var MAX_STORED = 50;
724
+ var TOKEN_HEADER = "X-Guidely-Conversation";
725
+ function storedConversations(store, projectId) {
726
+ const list = readJson(store, KEYS.conversations(projectId), []);
727
+ return Array.isArray(list) ? list.filter((c) => c && typeof c.id === "string" && typeof c.token === "string") : [];
728
+ }
729
+ function forgetConversations(store, projectId) {
730
+ store.remove(KEYS.conversations(projectId));
731
+ }
732
+ var HttpError = class extends Error {
733
+ constructor(status) {
734
+ super(`Request failed (${status})`);
735
+ __publicField(this, "status", status);
736
+ }
737
+ };
738
+ function createMessengerTransport(opts) {
739
+ const { api, store, projectId } = opts;
740
+ const tokenFor = (id) => storedConversations(store, projectId).find((c) => c.id === id)?.token ?? "";
741
+ const remember = (id, token) => {
742
+ const rest = storedConversations(store, projectId).filter((c) => c.id !== id);
743
+ writeJson(store, KEYS.conversations(projectId), [{ id, token }, ...rest].slice(0, MAX_STORED));
744
+ };
745
+ const forget = (id) => writeJson(store, KEYS.conversations(projectId), storedConversations(store, projectId).filter((c) => c.id !== id));
746
+ async function exchange(path, body, headers, h, onStarted) {
747
+ let finished = false;
748
+ const res = await api.stream(
749
+ path,
750
+ body,
751
+ (event, data) => {
752
+ if (event === "conversation") {
753
+ const d = data;
754
+ onStarted?.(d);
755
+ h.onConversation?.(d.conversation);
756
+ d.messages.forEach((m) => h.onMessage(m));
757
+ } else if (event === "message") h.onMessage(data);
758
+ else if (event === "delta") {
759
+ const d = data;
760
+ h.onDelta(d.messageId, d.text);
761
+ } else if (event === "done") {
762
+ const d = data;
763
+ finished = true;
764
+ h.onDone(d.message, d.conversation);
765
+ } else if (event === "error") h.onError(data?.message ?? "Something went wrong");
766
+ },
767
+ headers
768
+ );
769
+ if (!res.ok) throw new HttpError(res.status);
770
+ if (res.data) {
771
+ const d = res.data;
772
+ if ("token" in d) {
773
+ onStarted?.(d);
774
+ h.onConversation?.(d.conversation);
775
+ }
776
+ d.messages.forEach((m) => h.onMessage(m));
777
+ h.onDone(d.messages.at(-1) ?? null, d.conversation);
778
+ } else if (!finished) h.onError("The reply was interrupted");
779
+ }
780
+ return {
781
+ async listConversations() {
782
+ const tokens = storedConversations(store, projectId).map((c) => c.token);
783
+ if (!tokens.length) return [];
784
+ const res = await api.post("/conversations/lookup", { tokens });
785
+ if (!res.ok || !res.data) throw new HttpError(res.status);
786
+ const known = new Set(res.data.conversations.map((c) => c.id));
787
+ for (const c of storedConversations(store, projectId)) if (!known.has(c.id)) forget(c.id);
788
+ return res.data.conversations.map(({ token: _token, ...c }) => c);
789
+ },
790
+ async loadMessages(id, since) {
791
+ const res = await api.get(`/conversations/${encodeURIComponent(id)}/messages`, { params: { since: since ?? void 0 }, headers: { [TOKEN_HEADER]: tokenFor(id) } });
792
+ if (!res.ok || !res.data) throw new HttpError(res.status);
793
+ return res.data;
794
+ },
795
+ start(text, h) {
796
+ return exchange("/conversations", { anonymousId: opts.anonymousId(), userId: opts.userId(), message: text, pageUrl: opts.pageUrl() }, {}, h, (r) => remember(r.conversation.id, r.token));
797
+ },
798
+ send(id, text, h) {
799
+ return exchange(`/conversations/${encodeURIComponent(id)}/messages`, { message: text }, { [TOKEN_HEADER]: tokenFor(id) }, h);
800
+ },
801
+ async handoff(id) {
802
+ const res = await api.post(`/conversations/${encodeURIComponent(id)}/handoff`, {}, { headers: { [TOKEN_HEADER]: tokenFor(id) } });
803
+ if (!res.ok || !res.data) throw new HttpError(res.status);
804
+ return res.data;
805
+ },
806
+ async rate(id, rating) {
807
+ const res = await api.post(`/conversations/${encodeURIComponent(id)}/rating`, { rating }, { headers: { [TOKEN_HEADER]: tokenFor(id) } });
808
+ if (!res.ok) throw new HttpError(res.status);
809
+ },
810
+ async listArticles() {
811
+ const res = await api.get("/articles");
812
+ if (!res.ok || !res.data) throw new HttpError(res.status);
813
+ return res.data;
814
+ },
815
+ async getArticle(id) {
816
+ const res = await api.get(`/articles/${encodeURIComponent(id)}`);
817
+ return res.ok && res.data ? res.data.article : null;
818
+ },
819
+ async searchArticles(q) {
820
+ const res = await api.get("/resource-center/search", { params: { q } });
821
+ return res.ok && res.data ? res.data.articles : [];
822
+ }
823
+ };
824
+ }
541
825
 
542
826
  // src/navigation.ts
543
827
  function watchNavigation(listener, debounceMs = 50) {
@@ -709,7 +993,7 @@ var EventQueue = class {
709
993
 
710
994
  // src/client.ts
711
995
  var SDK_NAME = "@guidelytech/sdk-core";
712
- var SDK_VERSION = "0.1.0";
996
+ var SDK_VERSION = "0.2.1";
713
997
  var DEFAULT_API_URL = "https://api.guidely.app";
714
998
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
715
999
  function isBrowser() {
@@ -745,6 +1029,9 @@ var GuidelyClient = class _GuidelyClient {
745
1029
  __publicField(this, "widgets", /* @__PURE__ */ new Map());
746
1030
  __publicField(this, "resourceCenter", null);
747
1031
  __publicField(this, "resourceCenterScope", null);
1032
+ __publicField(this, "messenger", null);
1033
+ __publicField(this, "messengerScope", null);
1034
+ __publicField(this, "messengerLoading", null);
748
1035
  __publicField(this, "cleanups", []);
749
1036
  __publicField(this, "timers", /* @__PURE__ */ new Set());
750
1037
  __publicField(this, "visibilityObservers", /* @__PURE__ */ new Map());
@@ -753,6 +1040,8 @@ var GuidelyClient = class _GuidelyClient {
753
1040
  __publicField(this, "lastUrl");
754
1041
  __publicField(this, "authoring", null);
755
1042
  __publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
1043
+ /** Set once the app calls identify()/reset(); bootstrap then leaves identity alone instead of re-identifying. */
1044
+ __publicField(this, "identitySettled", false);
756
1045
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
757
1046
  __publicField(this, "eventBus", {
758
1047
  on: (name, cb) => {
@@ -787,7 +1076,7 @@ var GuidelyClient = class _GuidelyClient {
787
1076
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
788
1077
  this.anonymousId = this.store.get(KEYS.anon) || "";
789
1078
  if (!this.anonymousId) {
790
- this.anonymousId = `anon_${(0, import_runtime5.randomId)(20)}`;
1079
+ this.anonymousId = `anon_${(0, import_runtime6.randomId)(20)}`;
791
1080
  this.store.set(KEYS.anon, this.anonymousId);
792
1081
  }
793
1082
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -795,7 +1084,7 @@ var GuidelyClient = class _GuidelyClient {
795
1084
  this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(this.options.projectId), {}) };
796
1085
  this.user = readJson(this.store, KEYS.user(this.options.projectId), null);
797
1086
  const session = readJson(this.sessionStore, KEYS.session, null);
798
- this.session = session && session.id ? session : { id: `s_${(0, import_runtime5.randomId)(16)}`, startedAt: nowIso(), shownGuides: [], events: [] };
1087
+ this.session = session && session.id ? session : { id: `s_${(0, import_runtime6.randomId)(16)}`, startedAt: nowIso(), shownGuides: [], events: [] };
799
1088
  this.persistSession();
800
1089
  this.lastUrl = location.href;
801
1090
  this.lastAudiences = new Set(this.state.audienceIds);
@@ -836,8 +1125,10 @@ var GuidelyClient = class _GuidelyClient {
836
1125
  await this.fetchConfig();
837
1126
  if (this.destroyed) return;
838
1127
  this.wireListeners();
839
- if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
840
- else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1128
+ if (!this.identitySettled) {
1129
+ if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
1130
+ else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1131
+ }
841
1132
  if (this.destroyed) return;
842
1133
  this.isReady = true;
843
1134
  if (this.config) this.emitter.emit("ready", this.config);
@@ -929,6 +1220,7 @@ var GuidelyClient = class _GuidelyClient {
929
1220
  this.resourceCenter?.destroy();
930
1221
  this.resourceCenterScope?.destroy();
931
1222
  this.resourceCenter = null;
1223
+ this.destroyMessenger();
932
1224
  this.visibilityObservers.forEach((stop) => stop());
933
1225
  this.visibilityObservers.clear();
934
1226
  this.timers.forEach((t) => clearTimeout(t));
@@ -948,9 +1240,10 @@ var GuidelyClient = class _GuidelyClient {
948
1240
  /** Identifies the end user and loads server-held state (audiences, guide history, checklist progress). */
949
1241
  async identify(userId, traits, account) {
950
1242
  if (this.destroyed) return;
1243
+ this.identitySettled = true;
951
1244
  this.user = { id: String(userId), traits: { ...this.user?.id === String(userId) ? this.user.traits : {}, ...traits || {} }, account: account ?? (this.user?.id === String(userId) ? this.user.account : void 0) };
952
1245
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
953
- this.enqueue("identify", { properties: { traits: (0, import_runtime5.sanitizeProperties)(this.user.traits) } });
1246
+ this.enqueue("identify", { properties: { traits: (0, import_runtime6.sanitizeProperties)(this.user.traits) } });
954
1247
  await this.syncIdentify();
955
1248
  this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
956
1249
  if (this.isReady) this.evaluate("identify");
@@ -1013,20 +1306,23 @@ var GuidelyClient = class _GuidelyClient {
1013
1306
  }
1014
1307
  /** Forgets the user: new anonymous id, cleared local guide/checklist state, triggers re-evaluated. */
1015
1308
  reset() {
1309
+ this.identitySettled = true;
1016
1310
  this.stopGuide();
1017
1311
  this.user = null;
1018
1312
  this.store.remove(KEYS.user(this.options.projectId));
1019
- this.anonymousId = `anon_${(0, import_runtime5.randomId)(20)}`;
1313
+ this.anonymousId = `anon_${(0, import_runtime6.randomId)(20)}`;
1020
1314
  this.store.set(KEYS.anon, this.anonymousId);
1021
1315
  this.state = emptyState();
1022
1316
  this.persistState();
1023
- this.session = { id: `s_${(0, import_runtime5.randomId)(16)}`, startedAt: nowIso(), shownGuides: [], events: [] };
1317
+ this.session = { id: `s_${(0, import_runtime6.randomId)(16)}`, startedAt: nowIso(), shownGuides: [], events: [] };
1024
1318
  this.persistSession();
1025
1319
  this.sessionStore.remove(KEYS.active);
1026
1320
  this.lastAudiences = /* @__PURE__ */ new Set();
1027
1321
  this.firedDelays.clear();
1028
1322
  this.widgets.forEach((w) => w.destroy());
1029
1323
  this.widgets.clear();
1324
+ forgetConversations(this.store, this.options.projectId);
1325
+ this.destroyMessenger();
1030
1326
  this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
1031
1327
  if (this.isReady) {
1032
1328
  this.renderPersistentWidgets();
@@ -1051,7 +1347,7 @@ var GuidelyClient = class _GuidelyClient {
1051
1347
  enqueue(name, extra = {}) {
1052
1348
  if (this.consent === "denied" || !this.options.analytics) return null;
1053
1349
  const event = {
1054
- eventId: (0, import_runtime5.randomId)(24),
1350
+ eventId: (0, import_runtime6.randomId)(24),
1055
1351
  name,
1056
1352
  occurredAt: nowIso(),
1057
1353
  anonymousId: this.anonymousId,
@@ -1061,7 +1357,7 @@ var GuidelyClient = class _GuidelyClient {
1061
1357
  url: this.collectUrls() ? location.href : void 0,
1062
1358
  ...extra
1063
1359
  };
1064
- if (extra.properties) event.properties = (0, import_runtime5.sanitizeProperties)(extra.properties);
1360
+ if (extra.properties) event.properties = (0, import_runtime6.sanitizeProperties)(extra.properties);
1065
1361
  this.queue.enqueue(event);
1066
1362
  this.emitter.emit("track", event);
1067
1363
  return event;
@@ -1145,7 +1441,7 @@ var GuidelyClient = class _GuidelyClient {
1145
1441
  if (!isWithinSchedule(guide)) return false;
1146
1442
  if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
1147
1443
  ctx.rolloutSalt = guide.id;
1148
- return (0, import_runtime5.evaluateRules)(c.targeting, ctx);
1444
+ return (0, import_runtime6.evaluateRules)(c.targeting, ctx);
1149
1445
  }
1150
1446
  isEligible(guide, ctx, url) {
1151
1447
  if (!this.isTargeted(guide, ctx, url)) return false;
@@ -1432,7 +1728,7 @@ var GuidelyClient = class _GuidelyClient {
1432
1728
  title: guide.name,
1433
1729
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
1434
1730
  onSubmit: (answers) => {
1435
- const responseId = (0, import_runtime5.randomId)(24);
1731
+ const responseId = (0, import_runtime6.randomId)(24);
1436
1732
  this.state.surveysSubmitted.push(guide.id);
1437
1733
  this.persistState();
1438
1734
  this.recordEnd(guide, "completed");
@@ -1463,6 +1759,16 @@ var GuidelyClient = class _GuidelyClient {
1463
1759
  renderPersistentWidgets() {
1464
1760
  if (!this.config) return;
1465
1761
  const rc = this.config.resourceCenter;
1762
+ if (this.config.messenger?.enabled) {
1763
+ if (this.resourceCenter) {
1764
+ this.resourceCenter.destroy();
1765
+ this.resourceCenterScope?.destroy();
1766
+ this.resourceCenter = null;
1767
+ }
1768
+ if (!this.messenger) void this.loadMessenger();
1769
+ return;
1770
+ }
1771
+ this.destroyMessenger();
1466
1772
  if (rc && rc.enabled && !this.resourceCenter) {
1467
1773
  const scope = (0, import_guide_runtime3.createScope)(this.theme());
1468
1774
  this.resourceCenterScope = scope;
@@ -1471,6 +1777,7 @@ var GuidelyClient = class _GuidelyClient {
1471
1777
  config: rc,
1472
1778
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
1473
1779
  search: (q) => this.searchResourceCenter(q),
1780
+ loadArticle: (id) => this.loadArticle(id),
1474
1781
  onStartGuide: (id) => this.startGuide(id),
1475
1782
  onOpenChecklist: (id) => this.openChecklist(id),
1476
1783
  onArticleView: (a) => this.enqueue("article_viewed", { properties: { articleId: a.id, slug: a.slug } }),
@@ -1493,6 +1800,60 @@ var GuidelyClient = class _GuidelyClient {
1493
1800
  if (!guide || !guide.config.steps.some((s) => s.id === saved.stepId)) return;
1494
1801
  this.showFlow(guide, saved.manual ?? false, saved.stepId);
1495
1802
  }
1803
+ /** Loads the messenger widget on demand (it is a separate chunk) and renders it. */
1804
+ loadMessenger() {
1805
+ if (this.messenger) return Promise.resolve(this.messenger);
1806
+ if (this.messengerLoading) return this.messengerLoading;
1807
+ this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
1808
+ const cfg = this.config?.messenger;
1809
+ if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
1810
+ const scope = (0, import_guide_runtime3.createScope)(this.theme());
1811
+ this.messengerScope = scope;
1812
+ this.messenger = renderMessenger({
1813
+ scope: scope.el,
1814
+ config: cfg,
1815
+ api: createMessengerTransport({
1816
+ api: this.api,
1817
+ store: this.store,
1818
+ projectId: this.options.projectId,
1819
+ anonymousId: () => this.anonymousId,
1820
+ userId: () => this.user?.id || null,
1821
+ pageUrl: () => this.collectUrls() ? location.href : void 0
1822
+ }),
1823
+ resourceCenter: this.config.resourceCenter,
1824
+ guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
1825
+ onStartGuide: (idOrKey) => this.startGuide(idOrKey),
1826
+ onOpenChecklist: (id) => this.openChecklist(id),
1827
+ onArticleView: (a) => this.enqueue("article_viewed", { properties: { articleId: a.id, slug: a.slug } }),
1828
+ onOpen: () => {
1829
+ this.enqueue("messenger_opened");
1830
+ this.emitter.emit("messenger:open");
1831
+ },
1832
+ onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation })
1833
+ });
1834
+ return this.messenger;
1835
+ }).catch((err) => {
1836
+ this.reportError(err);
1837
+ return null;
1838
+ }).finally(() => {
1839
+ this.messengerLoading = null;
1840
+ });
1841
+ return this.messengerLoading;
1842
+ }
1843
+ destroyMessenger() {
1844
+ this.messenger?.destroy();
1845
+ this.messengerScope?.destroy();
1846
+ this.messenger = null;
1847
+ this.messengerScope = null;
1848
+ }
1849
+ async loadArticle(id) {
1850
+ try {
1851
+ const res = await this.api.get(`/articles/${encodeURIComponent(id)}`);
1852
+ return res.ok && res.data ? res.data.article : null;
1853
+ } catch {
1854
+ return null;
1855
+ }
1856
+ }
1496
1857
  async searchResourceCenter(q) {
1497
1858
  try {
1498
1859
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -1647,12 +2008,27 @@ var GuidelyClient = class _GuidelyClient {
1647
2008
  return !!this.state.guides[guide?.id ?? idOrKey]?.completedAt;
1648
2009
  }
1649
2010
  openResourceCenter() {
2011
+ if (this.config?.messenger?.enabled) return this.openMessenger("help");
1650
2012
  if (!this.resourceCenter) this.renderPersistentWidgets();
1651
2013
  this.resourceCenter?.open();
1652
2014
  }
1653
2015
  closeResourceCenter() {
2016
+ if (this.messenger) return this.closeMessenger();
1654
2017
  this.resourceCenter?.close();
1655
2018
  }
2019
+ /** Opens the Scout messenger (when enabled for the project), optionally on a tab. */
2020
+ openMessenger(tab) {
2021
+ if (!this.config?.messenger?.enabled) return;
2022
+ void this.loadMessenger().then((m) => m?.open(tab));
2023
+ }
2024
+ closeMessenger() {
2025
+ this.messenger?.close();
2026
+ }
2027
+ /** Opens a new messenger conversation, optionally pre-filling the message. */
2028
+ showNewMessage(text) {
2029
+ if (!this.config?.messenger?.enabled) return;
2030
+ void this.loadMessenger().then((m) => m?.showNewMessage(text));
2031
+ }
1656
2032
  on(event, cb) {
1657
2033
  return this.emitter.on(event, cb);
1658
2034
  }
@@ -1764,7 +2140,7 @@ var Guidely = {
1764
2140
  init: (options) => GuidelyClient.init(options),
1765
2141
  get: () => GuidelyClient.get(),
1766
2142
  createGuidely,
1767
- version: "0.1.0"
2143
+ version: SDK_VERSION
1768
2144
  };
1769
2145
  var index_default = Guidely;
1770
2146
  // Annotate the CommonJS export names for ESM import in node:
@@ -1780,6 +2156,7 @@ var index_default = Guidely;
1780
2156
  SDK_VERSION,
1781
2157
  areMutuallyExclusive,
1782
2158
  buildContext,
2159
+ captureSnapshot,
1783
2160
  createGuidely,
1784
2161
  createStorage,
1785
2162
  detectDevice,