@noodleseed/assistant 1.18.0 → 1.19.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 CHANGED
@@ -22567,7 +22567,122 @@ function radiusValue(radius) {
22567
22567
  return 14;
22568
22568
  }
22569
22569
 
22570
+ // src/model-context.ts
22571
+ var MAX_BYTES = 16 * 1024;
22572
+ var MAX_DEPTH = 8;
22573
+ var MAX_ENTRIES = 128;
22574
+ var SENSITIVE_KEY = /(?:secret|token|api[-_]?key|password|credential|authorization|cookie)/i;
22575
+ var CREDENTIAL_SHAPED_TEXT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\bsk-[A-Za-z0-9_-]{20,}\b|\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{20,}\b|\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b)/i;
22576
+ function copyAssistantModelContext(update) {
22577
+ const copied = copyJsonValue(update, "$", 0, /* @__PURE__ */ new Set());
22578
+ assertModelContextShape(copied);
22579
+ const encoded = JSON.stringify(copied);
22580
+ if (new TextEncoder().encode(encoded).byteLength > MAX_BYTES) {
22581
+ throw new Error("Model context must not exceed 16 KiB");
22582
+ }
22583
+ return copied;
22584
+ }
22585
+ function copyAssistantPageContext(context) {
22586
+ const copied = copyJsonValue(context, "$", 0, /* @__PURE__ */ new Set());
22587
+ if (typeof copied !== "object" || copied === null || Array.isArray(copied)) {
22588
+ throw new Error("Page context must be a JSON object");
22589
+ }
22590
+ const encoded = JSON.stringify(copied);
22591
+ if (new TextEncoder().encode(encoded).byteLength > MAX_BYTES) {
22592
+ throw new Error("Page context must not exceed 16 KiB");
22593
+ }
22594
+ return copied;
22595
+ }
22596
+ function assertModelContextShape(value) {
22597
+ if (!isPlainRecord(value)) throw new Error("Model context must be a JSON object");
22598
+ for (const key of Object.keys(value)) {
22599
+ if (key !== "content" && key !== "structuredContent") {
22600
+ throw new Error(`Model context contains unknown field ${key}`);
22601
+ }
22602
+ }
22603
+ if (value.content !== void 0) {
22604
+ if (!Array.isArray(value.content)) throw new Error("Model context content must be an array");
22605
+ for (const part of value.content) {
22606
+ if (!isPlainRecord(part) || Object.keys(part).some((key) => key !== "type" && key !== "text") || part.type !== "text" || typeof part.text !== "string") {
22607
+ throw new Error("Model context content supports text parts only");
22608
+ }
22609
+ }
22610
+ }
22611
+ if (value.structuredContent !== void 0 && !isPlainRecord(value.structuredContent)) {
22612
+ throw new Error("Model context structuredContent must be an object");
22613
+ }
22614
+ }
22615
+ function copyJsonValue(value, path, depth, ancestors) {
22616
+ if (depth > MAX_DEPTH) throw new Error(`Model context exceeds maximum depth at ${path}`);
22617
+ if (typeof value === "string") {
22618
+ if (CREDENTIAL_SHAPED_TEXT.test(value)) {
22619
+ throw new Error(`Model context contains credential-shaped text at ${path}`);
22620
+ }
22621
+ return value;
22622
+ }
22623
+ if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
22624
+ return value;
22625
+ }
22626
+ if (typeof value !== "object") throw new Error(`Model context contains non-JSON data at ${path}`);
22627
+ if (hasToJsonProperty(value)) {
22628
+ throw new Error(`Model context must not define or inherit toJSON at ${path}`);
22629
+ }
22630
+ const prototype = Object.getPrototypeOf(value);
22631
+ if (Array.isArray(value)) {
22632
+ if (prototype !== Array.prototype) {
22633
+ throw new Error(`Model context contains a non-JSON object at ${path}`);
22634
+ }
22635
+ } else if (prototype !== Object.prototype && prototype !== null) {
22636
+ throw new Error(`Model context contains a non-JSON object at ${path}`);
22637
+ }
22638
+ if (ancestors.has(value)) throw new Error(`Model context contains a cycle at ${path}`);
22639
+ if (Array.isArray(value)) {
22640
+ if (value.length > MAX_ENTRIES) {
22641
+ throw new Error(`Model context has more than 128 entries at ${path}`);
22642
+ }
22643
+ const copy2 = [];
22644
+ ancestors.add(value);
22645
+ for (let index = 0; index < value.length; index += 1) {
22646
+ const entry = Object.hasOwn(value, index) ? value[index] : null;
22647
+ copy2.push(copyJsonValue(entry, `${path}.${index}`, depth + 1, ancestors));
22648
+ }
22649
+ ancestors.delete(value);
22650
+ return copy2;
22651
+ }
22652
+ const entries2 = Object.entries(value);
22653
+ if (entries2.length > MAX_ENTRIES) {
22654
+ throw new Error(`Model context has more than 128 entries at ${path}`);
22655
+ }
22656
+ const copy = /* @__PURE__ */ Object.create(null);
22657
+ ancestors.add(value);
22658
+ for (const [key, entry] of entries2) {
22659
+ if (SENSITIVE_KEY.test(key)) {
22660
+ throw new Error(`Model context contains sensitive key ${path}.${key}`);
22661
+ }
22662
+ copy[key] = copyJsonValue(entry, `${path}.${key}`, depth + 1, ancestors);
22663
+ }
22664
+ ancestors.delete(value);
22665
+ return copy;
22666
+ }
22667
+ function isPlainRecord(value) {
22668
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
22669
+ const prototype = Object.getPrototypeOf(value);
22670
+ return prototype === Object.prototype || prototype === null;
22671
+ }
22672
+ function hasToJsonProperty(value) {
22673
+ let current = value;
22674
+ const visited = /* @__PURE__ */ new Set();
22675
+ while (current !== null && !visited.has(current)) {
22676
+ visited.add(current);
22677
+ if (Object.getOwnPropertyDescriptor(current, "toJSON") !== void 0) return true;
22678
+ current = Object.getPrototypeOf(current);
22679
+ }
22680
+ return false;
22681
+ }
22682
+
22570
22683
  // src/browser-context.ts
22684
+ var PAGE_MARKDOWN_MAX_BYTES = 12 * 1024;
22685
+ var PAGE_MARKDOWN_TIMEOUT_MS = 2e3;
22571
22686
  function browserClientContext() {
22572
22687
  let timeZone;
22573
22688
  try {
@@ -22580,6 +22695,108 @@ function browserClientContext() {
22580
22695
  ...timeZone ? { timeZone } : {}
22581
22696
  };
22582
22697
  }
22698
+ function browserPageUrl() {
22699
+ try {
22700
+ const location = globalThis.location;
22701
+ if (!location) return void 0;
22702
+ const url2 = new URL(location.href);
22703
+ if (url2.origin === "null") return void 0;
22704
+ return `${url2.origin}${url2.pathname}`;
22705
+ } catch {
22706
+ return void 0;
22707
+ }
22708
+ }
22709
+ async function browserPageContext(url2) {
22710
+ let urlOnly;
22711
+ try {
22712
+ urlOnly = copyAssistantPageContext({ page: { url: url2 } });
22713
+ } catch {
22714
+ return void 0;
22715
+ }
22716
+ const controller = new AbortController();
22717
+ const timeout = globalThis.setTimeout(() => controller.abort(), PAGE_MARKDOWN_TIMEOUT_MS);
22718
+ try {
22719
+ const response = await fetch(url2, {
22720
+ headers: { Accept: "text/markdown" },
22721
+ credentials: "omit",
22722
+ cache: "no-store",
22723
+ redirect: "error",
22724
+ referrerPolicy: "no-referrer",
22725
+ signal: controller.signal
22726
+ });
22727
+ if (!response.ok || !isSameOrigin(response, url2) || !isMarkdown(response)) return urlOnly;
22728
+ const { content, truncated } = await readBoundedUtf8(response);
22729
+ const page = {
22730
+ page: {
22731
+ url: url2,
22732
+ contentType: "text/markdown",
22733
+ content,
22734
+ ...truncated ? { truncated: true } : {}
22735
+ }
22736
+ };
22737
+ return copyAssistantPageContext(page);
22738
+ } catch {
22739
+ return urlOnly;
22740
+ } finally {
22741
+ globalThis.clearTimeout(timeout);
22742
+ }
22743
+ }
22744
+ function isSameOrigin(response, url2) {
22745
+ try {
22746
+ return response.url !== "" && new URL(response.url).origin === new URL(url2).origin;
22747
+ } catch {
22748
+ return false;
22749
+ }
22750
+ }
22751
+ function isMarkdown(response) {
22752
+ return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === "text/markdown";
22753
+ }
22754
+ async function readBoundedUtf8(response) {
22755
+ const reader = response.body?.getReader();
22756
+ if (!reader) throw new Error("Markdown response has no readable body");
22757
+ const chunks = [];
22758
+ let byteLength = 0;
22759
+ let truncated = false;
22760
+ try {
22761
+ while (true) {
22762
+ const { done, value } = await reader.read();
22763
+ if (done) break;
22764
+ const remaining = PAGE_MARKDOWN_MAX_BYTES - byteLength;
22765
+ if (value.byteLength > remaining) {
22766
+ if (remaining > 0) chunks.push(value.slice(0, remaining));
22767
+ truncated = true;
22768
+ await reader.cancel();
22769
+ break;
22770
+ }
22771
+ chunks.push(value);
22772
+ byteLength += value.byteLength;
22773
+ }
22774
+ } finally {
22775
+ reader.releaseLock();
22776
+ }
22777
+ const bytes = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));
22778
+ let offset = 0;
22779
+ for (const chunk of chunks) {
22780
+ bytes.set(chunk, offset);
22781
+ offset += chunk.byteLength;
22782
+ }
22783
+ return { content: decodeUtf8(bytes, truncated), truncated };
22784
+ }
22785
+ function decodeUtf8(bytes, truncated) {
22786
+ const decoder = new TextDecoder("utf-8", { fatal: true });
22787
+ let content;
22788
+ try {
22789
+ content = decoder.decode(bytes, { stream: true });
22790
+ } catch {
22791
+ throw new Error("Markdown response is not valid UTF-8");
22792
+ }
22793
+ try {
22794
+ return content + decoder.decode();
22795
+ } catch {
22796
+ if (truncated) return content;
22797
+ throw new Error("Markdown response is not valid UTF-8");
22798
+ }
22799
+ }
22583
22800
 
22584
22801
  // ../../node_modules/.pnpm/@ai-sdk+provider@3.0.10/node_modules/@ai-sdk/provider/dist/index.mjs
22585
22802
  var marker = "vercel.ai.error";
@@ -27168,119 +27385,6 @@ function isRecord2(value) {
27168
27385
  return typeof value === "object" && value !== null && !Array.isArray(value);
27169
27386
  }
27170
27387
 
27171
- // src/model-context.ts
27172
- var MAX_BYTES = 16 * 1024;
27173
- var MAX_DEPTH = 8;
27174
- var MAX_ENTRIES = 128;
27175
- var SENSITIVE_KEY = /(?:secret|token|api[-_]?key|password|credential|authorization|cookie)/i;
27176
- var CREDENTIAL_SHAPED_TEXT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\bsk-[A-Za-z0-9_-]{20,}\b|\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{20,}\b|\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b)/i;
27177
- function copyAssistantModelContext(update) {
27178
- const copied = copyJsonValue(update, "$", 0, /* @__PURE__ */ new Set());
27179
- assertModelContextShape(copied);
27180
- const encoded = JSON.stringify(copied);
27181
- if (new TextEncoder().encode(encoded).byteLength > MAX_BYTES) {
27182
- throw new Error("Model context must not exceed 16 KiB");
27183
- }
27184
- return copied;
27185
- }
27186
- function copyAssistantPageContext(context) {
27187
- const copied = copyJsonValue(context, "$", 0, /* @__PURE__ */ new Set());
27188
- if (typeof copied !== "object" || copied === null || Array.isArray(copied)) {
27189
- throw new Error("Page context must be a JSON object");
27190
- }
27191
- const encoded = JSON.stringify(copied);
27192
- if (new TextEncoder().encode(encoded).byteLength > MAX_BYTES) {
27193
- throw new Error("Page context must not exceed 16 KiB");
27194
- }
27195
- return copied;
27196
- }
27197
- function assertModelContextShape(value) {
27198
- if (!isPlainRecord(value)) throw new Error("Model context must be a JSON object");
27199
- for (const key of Object.keys(value)) {
27200
- if (key !== "content" && key !== "structuredContent") {
27201
- throw new Error(`Model context contains unknown field ${key}`);
27202
- }
27203
- }
27204
- if (value.content !== void 0) {
27205
- if (!Array.isArray(value.content)) throw new Error("Model context content must be an array");
27206
- for (const part of value.content) {
27207
- if (!isPlainRecord(part) || Object.keys(part).some((key) => key !== "type" && key !== "text") || part.type !== "text" || typeof part.text !== "string") {
27208
- throw new Error("Model context content supports text parts only");
27209
- }
27210
- }
27211
- }
27212
- if (value.structuredContent !== void 0 && !isPlainRecord(value.structuredContent)) {
27213
- throw new Error("Model context structuredContent must be an object");
27214
- }
27215
- }
27216
- function copyJsonValue(value, path, depth, ancestors) {
27217
- if (depth > MAX_DEPTH) throw new Error(`Model context exceeds maximum depth at ${path}`);
27218
- if (typeof value === "string") {
27219
- if (CREDENTIAL_SHAPED_TEXT.test(value)) {
27220
- throw new Error(`Model context contains credential-shaped text at ${path}`);
27221
- }
27222
- return value;
27223
- }
27224
- if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
27225
- return value;
27226
- }
27227
- if (typeof value !== "object") throw new Error(`Model context contains non-JSON data at ${path}`);
27228
- if (hasToJsonProperty(value)) {
27229
- throw new Error(`Model context must not define or inherit toJSON at ${path}`);
27230
- }
27231
- const prototype = Object.getPrototypeOf(value);
27232
- if (Array.isArray(value)) {
27233
- if (prototype !== Array.prototype) {
27234
- throw new Error(`Model context contains a non-JSON object at ${path}`);
27235
- }
27236
- } else if (prototype !== Object.prototype && prototype !== null) {
27237
- throw new Error(`Model context contains a non-JSON object at ${path}`);
27238
- }
27239
- if (ancestors.has(value)) throw new Error(`Model context contains a cycle at ${path}`);
27240
- if (Array.isArray(value)) {
27241
- if (value.length > MAX_ENTRIES) {
27242
- throw new Error(`Model context has more than 128 entries at ${path}`);
27243
- }
27244
- const copy2 = [];
27245
- ancestors.add(value);
27246
- for (let index = 0; index < value.length; index += 1) {
27247
- const entry = Object.hasOwn(value, index) ? value[index] : null;
27248
- copy2.push(copyJsonValue(entry, `${path}.${index}`, depth + 1, ancestors));
27249
- }
27250
- ancestors.delete(value);
27251
- return copy2;
27252
- }
27253
- const entries2 = Object.entries(value);
27254
- if (entries2.length > MAX_ENTRIES) {
27255
- throw new Error(`Model context has more than 128 entries at ${path}`);
27256
- }
27257
- const copy = /* @__PURE__ */ Object.create(null);
27258
- ancestors.add(value);
27259
- for (const [key, entry] of entries2) {
27260
- if (SENSITIVE_KEY.test(key)) {
27261
- throw new Error(`Model context contains sensitive key ${path}.${key}`);
27262
- }
27263
- copy[key] = copyJsonValue(entry, `${path}.${key}`, depth + 1, ancestors);
27264
- }
27265
- ancestors.delete(value);
27266
- return copy;
27267
- }
27268
- function isPlainRecord(value) {
27269
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
27270
- const prototype = Object.getPrototypeOf(value);
27271
- return prototype === Object.prototype || prototype === null;
27272
- }
27273
- function hasToJsonProperty(value) {
27274
- let current = value;
27275
- const visited = /* @__PURE__ */ new Set();
27276
- while (current !== null && !visited.has(current)) {
27277
- visited.add(current);
27278
- if (Object.getOwnPropertyDescriptor(current, "toJSON") !== void 0) return true;
27279
- current = Object.getPrototypeOf(current);
27280
- }
27281
- return false;
27282
- }
27283
-
27284
27388
  // src/session-source.ts
27285
27389
  var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
27286
27390
  var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
@@ -31365,6 +31469,46 @@ function queryRequired(root, selector) {
31365
31469
  return element;
31366
31470
  }
31367
31471
 
31472
+ // src/element-page-context-controller.ts
31473
+ var AssistantElementPageContextController = class {
31474
+ #snapshot;
31475
+ #load;
31476
+ #isEnabled;
31477
+ constructor(isEnabled) {
31478
+ this.#isEnabled = isEnabled;
31479
+ }
31480
+ current() {
31481
+ const url2 = browserPageUrl();
31482
+ if (!url2 || this.#snapshot?.url !== url2) {
31483
+ this.#snapshot = void 0;
31484
+ return void 0;
31485
+ }
31486
+ return this.#snapshot.context;
31487
+ }
31488
+ async refresh() {
31489
+ while (true) {
31490
+ const url2 = browserPageUrl();
31491
+ if (!url2) return;
31492
+ if (this.#snapshot?.url === url2) return;
31493
+ this.#snapshot = void 0;
31494
+ let load = this.#load;
31495
+ if (!load || load.url !== url2) {
31496
+ const promise2 = browserPageContext(url2).then((context) => {
31497
+ if (this.#isEnabled() && browserPageUrl() === url2) {
31498
+ this.#snapshot = { url: url2, context };
31499
+ }
31500
+ }).finally(() => {
31501
+ if (this.#load?.promise === promise2) this.#load = void 0;
31502
+ });
31503
+ load = { url: url2, promise: promise2 };
31504
+ this.#load = load;
31505
+ }
31506
+ await load.promise;
31507
+ if (browserPageUrl() === url2) return;
31508
+ }
31509
+ }
31510
+ };
31511
+
31368
31512
  // src/element-scroll-controller.ts
31369
31513
  var AssistantElementScrollController = class {
31370
31514
  #region;
@@ -32105,6 +32249,9 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32105
32249
  #context;
32106
32250
  #modelContext;
32107
32251
  #pageContext;
32252
+ #automaticPageContext = new AssistantElementPageContextController(
32253
+ () => !this.#pageContext
32254
+ );
32108
32255
  #pendingUserEcho;
32109
32256
  #sessionState = "idle";
32110
32257
  #startOpenApplied = false;
@@ -32183,10 +32330,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32183
32330
  if (this.hasAttribute("start-open")) {
32184
32331
  this.setAttribute("open", "");
32185
32332
  this.#startOpenApplied = true;
32186
- this.#primeSession();
32187
32333
  } else if (!this.embedId) {
32188
32334
  this.#primeSession();
32189
32335
  }
32336
+ this.#syncOpenState();
32190
32337
  }
32191
32338
  disconnectedCallback() {
32192
32339
  this.#conversationGeneration += 1;
@@ -32208,7 +32355,6 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32208
32355
  }
32209
32356
  open() {
32210
32357
  this.setAttribute("open", "");
32211
- this.#primeSession();
32212
32358
  this.dispatchEvent(new CustomEvent("assistant-open"));
32213
32359
  }
32214
32360
  close() {
@@ -32302,6 +32448,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32302
32448
  try {
32303
32449
  await this.#sessionBootstrap;
32304
32450
  if (generation !== this.#conversationGeneration) return;
32451
+ if (this.embedId && !this.#pageContext) await this.#automaticPageContext.refresh();
32452
+ if (generation !== this.#conversationGeneration) return;
32305
32453
  await this.#ensureClient().sendMessage(message);
32306
32454
  } catch (error51) {
32307
32455
  if (generation !== this.#conversationGeneration) return;
@@ -32359,7 +32507,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32359
32507
  const behavior = {
32360
32508
  ...this.#context ? { context: this.#context } : {},
32361
32509
  clientContext: browserClientContext,
32362
- ...this.#pageContext ? { pageContext: this.#pageContext } : {},
32510
+ ...this.#pageContext ? { pageContext: this.#pageContext } : this.embedId ? { pageContext: () => this.#automaticPageContext.current() } : {},
32363
32511
  ...this.#modelContext ? { modelContext: this.#modelContext } : {}
32364
32512
  };
32365
32513
  const client = this.embedId ? createAssistantClient({
@@ -32377,6 +32525,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32377
32525
  #primeSession() {
32378
32526
  if (!this.sessionEndpoint && !this.embedId) return;
32379
32527
  if (this.#sessionBootstrap || this.#client?.hasSession()) return;
32528
+ if (this.embedId && !this.#pageContext) void this.#automaticPageContext.refresh();
32380
32529
  const client = this.#ensureClient();
32381
32530
  const generation = this.#conversationGeneration;
32382
32531
  this.#setSessionState("loading");
@@ -32560,7 +32709,9 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32560
32709
  syncAssistantSessionVisualState(this, this.shadowRoot, state, this.#appearance);
32561
32710
  }
32562
32711
  #syncOpenState() {
32563
- this.shadowRoot?.querySelector(".launcher")?.setAttribute("aria-expanded", String(this.hasAttribute("open")));
32712
+ const open = this.hasAttribute("open");
32713
+ this.shadowRoot?.querySelector(".launcher")?.setAttribute("aria-expanded", String(open));
32714
+ if (open && this.isConnected) this.#primeSession();
32564
32715
  }
32565
32716
  #closeAndRestoreFocus() {
32566
32717
  this.close();