@abraca/mcp 2.4.0 → 2.5.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.
@@ -4,7 +4,7 @@ import addFormats from "ajv-formats";
4
4
  import { ZodOptional, z } from "zod";
5
5
  import process$1 from "node:process";
6
6
  import * as Y from "yjs";
7
- import { AbracadabraClient, AbracadabraProvider, Kind, SERVER_ROOT_ID, awarenessStatesToArray } from "@abraca/dabra";
7
+ import { AbracadabraClient, AbracadabraProvider, Kind, SERVER_ROOT_ID, awarenessStatesToArray, foldRecords, isEncryptedContent, makeEntryMap, patchEntry, recordFromYAny, toPlain } from "@abraca/dabra";
8
8
  import { mkdir, readFile, writeFile } from "node:fs/promises";
9
9
  import * as fs from "node:fs";
10
10
  import { existsSync, readFileSync } from "node:fs";
@@ -14086,6 +14086,9 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14086
14086
  static {
14087
14087
  this.TOOL_HISTORY_MAX = 20;
14088
14088
  }
14089
+ static {
14090
+ this.DEDUPE_MAX = 1e3;
14091
+ }
14089
14092
  constructor(config) {
14090
14093
  this._serverInfo = null;
14091
14094
  this._rootDocId = null;
@@ -14103,6 +14106,14 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14103
14106
  this._lastChatChannel = null;
14104
14107
  this._signFn = null;
14105
14108
  this._toolHistory = [];
14109
+ this._inboxStarted = false;
14110
+ this._inboxDocId = null;
14111
+ this._inboxDoc = null;
14112
+ this._inboxProvider = null;
14113
+ this._inboxDisposers = [];
14114
+ this._inboxInitialized = false;
14115
+ this._seenInboxIds = /* @__PURE__ */ new Set();
14116
+ this._dispatchedMessageIds = /* @__PURE__ */ new Set();
14106
14117
  this.config = config;
14107
14118
  this.client = new AbracadabraClient({
14108
14119
  url: config.url,
@@ -14332,6 +14343,182 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14332
14343
  this._handleStatelessChat(payload);
14333
14344
  });
14334
14345
  console.error("[abracadabra-mcp] Stateless chat listener attached");
14346
+ this._startInboxNotifications();
14347
+ }
14348
+ }
14349
+ /**
14350
+ * Bootstrap inbox observation. Sends `messages:inbox_fetch` over the active
14351
+ * provider; the server's `messages:inbox_history` reply carries the
14352
+ * `inbox_doc_id`. We then open a dedicated provider on that doc and observe
14353
+ * its `entries` Y.Array for live DM/mention dispatch. Mirrors the
14354
+ * dashboard's `useNotifications` pattern.
14355
+ */
14356
+ async _startInboxNotifications() {
14357
+ if (this._inboxStarted) return;
14358
+ const provider = this._activeConnection?.provider;
14359
+ if (!provider) return;
14360
+ this._inboxStarted = true;
14361
+ provider.on("stateless", ({ payload }) => {
14362
+ if (!payload.includes("\"messages:inbox_history\"")) return;
14363
+ try {
14364
+ const data = JSON.parse(payload);
14365
+ if (data?.type !== "messages:inbox_history") return;
14366
+ const inboxDocId = typeof data.inbox_doc_id === "string" ? data.inbox_doc_id : null;
14367
+ if (inboxDocId) this._ensureInboxObserver(inboxDocId);
14368
+ } catch {}
14369
+ });
14370
+ provider.sendStateless(JSON.stringify({
14371
+ type: "messages:inbox_fetch",
14372
+ limit: 50,
14373
+ unread_only: false
14374
+ }));
14375
+ console.error("[abracadabra-mcp] Inbox bootstrap sent (messages:inbox_fetch)");
14376
+ }
14377
+ /**
14378
+ * Open a dedicated provider on the agent's inbox doc and observe its
14379
+ * `entries` Y.Array. The server is the only writer; we only read.
14380
+ */
14381
+ async _ensureInboxObserver(inboxDocId) {
14382
+ if (this._inboxDocId) return;
14383
+ this._inboxDocId = inboxDocId;
14384
+ if (!this.client.isTokenValid() && this._signFn && this._userId) await this.client.loginWithKey(this._userId, this._signFn);
14385
+ const doc = new Y.Doc({ guid: inboxDocId });
14386
+ const provider = new AbracadabraProvider({
14387
+ name: inboxDocId,
14388
+ document: doc,
14389
+ client: this.client,
14390
+ disableOfflineStore: true,
14391
+ subdocLoading: "lazy"
14392
+ });
14393
+ this._inboxDoc = doc;
14394
+ this._inboxProvider = provider;
14395
+ try {
14396
+ await waitForSync(provider);
14397
+ } catch (err) {
14398
+ console.error(`[abracadabra-mcp] Inbox sync failed: ${err?.message ?? err}`);
14399
+ return;
14400
+ }
14401
+ const entriesArr = doc.getArray("entries");
14402
+ const readMap = doc.getMap("read");
14403
+ const onChange = () => {
14404
+ this._pumpInbox(entriesArr, readMap);
14405
+ };
14406
+ entriesArr.observe(onChange);
14407
+ readMap.observe(onChange);
14408
+ this._inboxDisposers.push(() => entriesArr.unobserve(onChange));
14409
+ this._inboxDisposers.push(() => readMap.unobserve(onChange));
14410
+ await this._pumpInbox(entriesArr, readMap);
14411
+ console.error(`[abracadabra-mcp] Inbox observer attached (${inboxDocId})`);
14412
+ }
14413
+ /**
14414
+ * Diff the inbox `entries` array against what we've already seen. On the
14415
+ * first pump we only record a baseline (don't replay history). New entries
14416
+ * are classified by their authoritative `kind` and dispatched.
14417
+ */
14418
+ async _pumpInbox(entriesArr, readMap) {
14419
+ const entries = entriesArr.toArray().map((e) => typeof e?.toJSON === "function" ? e.toJSON() : e);
14420
+ if (!this._inboxInitialized) {
14421
+ for (const e of entries) if (e?.id) this._seenInboxIds.add(e.id);
14422
+ this._inboxInitialized = true;
14423
+ console.error(`[abracadabra-mcp] Inbox baseline: ${this._seenInboxIds.size} existing entries (not replayed)`);
14424
+ return;
14425
+ }
14426
+ for (const e of entries) {
14427
+ const id = e?.id;
14428
+ if (!id || this._seenInboxIds.has(id)) continue;
14429
+ this._seenInboxIds.add(id);
14430
+ if (readMap.get(id)) continue;
14431
+ try {
14432
+ await this._dispatchInboxEntry(e);
14433
+ } catch (err) {
14434
+ console.error(`[abracadabra-mcp] Inbox dispatch failed for ${id}: ${err?.message ?? err}`);
14435
+ }
14436
+ }
14437
+ this._trimSet(this._seenInboxIds);
14438
+ }
14439
+ /** Classify + dispatch one inbox entry as a channel notification. */
14440
+ async _dispatchInboxEntry(entry) {
14441
+ if (!this._serverRef) return;
14442
+ const kind = typeof entry?.kind === "string" ? entry.kind : "";
14443
+ const channelDocId = typeof entry?.channel_doc_id === "string" ? entry.channel_doc_id : "";
14444
+ const messageId = typeof entry?.message_id === "string" ? entry.message_id : null;
14445
+ const senderId = typeof entry?.sender_id === "string" ? entry.sender_id : "";
14446
+ if (!channelDocId) return;
14447
+ if (senderId && senderId === this._userId) return;
14448
+ const mode = this.triggerMode;
14449
+ if (kind === "mention" || kind === "reply") {
14450
+ if (mode === "task") return;
14451
+ } else if (kind !== "dm") return;
14452
+ if (messageId && this._rememberDispatched(messageId)) return;
14453
+ const content = await this._resolveInboxContent(channelDocId, messageId, entry?.preview);
14454
+ this._lastChatChannel = channelDocId;
14455
+ this._beginTurn();
14456
+ this.setAutoStatus("thinking");
14457
+ await this._serverRef.notification({
14458
+ method: "notifications/claude/channel",
14459
+ params: {
14460
+ content,
14461
+ instructions: `You MUST use send_chat_message with channel_doc_id="${channelDocId}" for ALL responses — both progress updates and final answers. The user CANNOT see plain text output; they only see messages sent via send_chat_message. When doing multi-step work, send brief status updates via send_chat_message (e.g. "Looking into that..." or "Found it, writing up results...") so the user knows you're working. Never output plain text as a substitute for send_chat_message.`,
14462
+ meta: {
14463
+ source: "abracadabra",
14464
+ type: kind === "dm" ? "dm_message" : "chat_message",
14465
+ channel_doc_id: channelDocId,
14466
+ sender: entry?.sender_name ?? "Unknown",
14467
+ sender_id: senderId,
14468
+ doc_id: channelDocId
14469
+ }
14470
+ }
14471
+ });
14472
+ console.error(`[abracadabra-mcp] Dispatched ${kind} on ${channelDocId} from ${entry?.sender_name ?? (senderId || "unknown")}`);
14473
+ this._activeConnection?.provider?.sendStateless(JSON.stringify({
14474
+ type: "messages:inbox_mark_read",
14475
+ id: entry.id
14476
+ }));
14477
+ }
14478
+ /**
14479
+ * Resolve full message content. The inbox `preview` is truncated to ≤200
14480
+ * bytes (and null for E2E), so for fidelity we read the actual record from
14481
+ * the channel/DM doc's active period — same shape the dashboard reads.
14482
+ * Falls back to the preview, then a short notice.
14483
+ */
14484
+ async _resolveInboxContent(channelDocId, messageId, preview) {
14485
+ const previewStr = typeof preview === "string" && preview.length > 0 ? preview : null;
14486
+ const root = this._activeConnection?.provider;
14487
+ if (root && messageId) try {
14488
+ const wrapper = await root.loadChild(channelDocId);
14489
+ if (!wrapper.synced) await waitForSync(wrapper);
14490
+ const periods = wrapper.document.getArray("periods");
14491
+ const len = periods.length;
14492
+ for (let i = len - 1; i >= 0 && i >= len - 2; i--) {
14493
+ const p = periods.get(i);
14494
+ const periodId = p?.id ?? p?.get?.("id");
14495
+ if (!periodId) continue;
14496
+ const period = await root.loadChild(periodId);
14497
+ if (!period.synced) await waitForSync(period);
14498
+ const hit = foldRecords(period.document.getArray("messages").toArray().map((v) => recordFromYAny(v)).filter((r) => r !== null)).find((f) => f.id === messageId);
14499
+ if (hit) {
14500
+ if (isEncryptedContent(hit.content)) return previewStr ?? "[encrypted message — this agent is not provisioned to read this channel]";
14501
+ return hit.content;
14502
+ }
14503
+ }
14504
+ } catch (err) {
14505
+ console.error(`[abracadabra-mcp] content read failed for ${channelDocId}/${messageId}: ${err?.message ?? err}`);
14506
+ }
14507
+ return previewStr ?? "[message content unavailable]";
14508
+ }
14509
+ _rememberDispatched(messageId) {
14510
+ if (this._dispatchedMessageIds.has(messageId)) return true;
14511
+ this._dispatchedMessageIds.add(messageId);
14512
+ this._trimSet(this._dispatchedMessageIds);
14513
+ return false;
14514
+ }
14515
+ _trimSet(s) {
14516
+ if (s.size <= AbracadabraMCPServer.DEDUPE_MAX) return;
14517
+ const excess = s.size - AbracadabraMCPServer.DEDUPE_MAX;
14518
+ let i = 0;
14519
+ for (const v of s) {
14520
+ if (i++ >= excess) break;
14521
+ s.delete(v);
14335
14522
  }
14336
14523
  }
14337
14524
  /** Attach awareness observer to detect `ai:task` fields from human users. */
@@ -14417,6 +14604,7 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14417
14604
  if (data.sender_id && data.sender_id === this._userId) return;
14418
14605
  const channelDocId = data.channel_doc_id;
14419
14606
  if (!channelDocId) return;
14607
+ if (data.id && this._rememberDispatched(data.id)) return;
14420
14608
  const mode = this.triggerMode;
14421
14609
  const content = typeof data.content === "string" ? data.content : "";
14422
14610
  let dispatchContent = content;
@@ -14567,6 +14755,13 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14567
14755
  clearInterval(this.evictionTimer);
14568
14756
  this.evictionTimer = null;
14569
14757
  }
14758
+ for (const dispose of this._inboxDisposers) try {
14759
+ dispose();
14760
+ } catch {}
14761
+ this._inboxDisposers = [];
14762
+ this._inboxProvider?.destroy();
14763
+ this._inboxProvider = null;
14764
+ this._inboxDoc = null;
14570
14765
  for (const [, cached] of this.childCache) cached.provider.destroy();
14571
14766
  this.childCache.clear();
14572
14767
  for (const [, conn] of this._spaceConnections) {
@@ -15327,13 +15522,13 @@ function normalizeRootId(id, server) {
15327
15522
  return id === server.rootDocId ? null : id;
15328
15523
  }
15329
15524
  /** Safely read a tree map value, converting Y.Map to plain object if needed. */
15330
- function toPlain(val) {
15525
+ function toPlain$1(val) {
15331
15526
  return val instanceof Y.Map ? val.toJSON() : val;
15332
15527
  }
15333
15528
  function readEntries$1(treeMap) {
15334
15529
  const entries = [];
15335
15530
  treeMap.forEach((raw, id) => {
15336
- const value = toPlain(raw);
15531
+ const value = toPlain$1(raw);
15337
15532
  if (typeof value !== "object" || value === null) return;
15338
15533
  entries.push({
15339
15534
  id,
@@ -15530,7 +15725,7 @@ function registerTreeTools(mcp, server, validator = null) {
15530
15725
  const normalizedParent = normalizeRootId(parentId, server);
15531
15726
  const now = Date.now();
15532
15727
  rootDoc.transact(() => {
15533
- treeMap.set(id, {
15728
+ treeMap.set(id, makeEntryMap({
15534
15729
  label,
15535
15730
  parentId: normalizedParent,
15536
15731
  order: now,
@@ -15538,7 +15733,7 @@ function registerTreeTools(mcp, server, validator = null) {
15538
15733
  meta,
15539
15734
  createdAt: now,
15540
15735
  updatedAt: now
15541
- });
15736
+ }));
15542
15737
  });
15543
15738
  server.setFocusedDoc(id);
15544
15739
  return { content: [{
@@ -15565,14 +15760,11 @@ function registerTreeTools(mcp, server, validator = null) {
15565
15760
  type: "text",
15566
15761
  text: "Not connected"
15567
15762
  }] };
15568
- const raw = treeMap.get(id);
15569
- if (!raw) return { content: [{
15763
+ if (!treeMap.get(id)) return { content: [{
15570
15764
  type: "text",
15571
15765
  text: `Document ${id} not found`
15572
15766
  }] };
15573
- const entry = toPlain(raw);
15574
- treeMap.set(id, {
15575
- ...entry,
15767
+ patchEntry(treeMap, id, {
15576
15768
  label,
15577
15769
  updatedAt: Date.now()
15578
15770
  });
@@ -15596,14 +15788,11 @@ function registerTreeTools(mcp, server, validator = null) {
15596
15788
  type: "text",
15597
15789
  text: "Not connected"
15598
15790
  }] };
15599
- const raw = treeMap.get(id);
15600
- if (!raw) return { content: [{
15791
+ if (!treeMap.get(id)) return { content: [{
15601
15792
  type: "text",
15602
15793
  text: `Document ${id} not found`
15603
15794
  }] };
15604
- const entry = toPlain(raw);
15605
- treeMap.set(id, {
15606
- ...entry,
15795
+ patchEntry(treeMap, id, {
15607
15796
  parentId: normalizeRootId(newParentId, server),
15608
15797
  order: order ?? Date.now(),
15609
15798
  updatedAt: Date.now()
@@ -15632,7 +15821,7 @@ function registerTreeTools(mcp, server, validator = null) {
15632
15821
  for (const nid of toDelete) {
15633
15822
  const raw = treeMap.get(nid);
15634
15823
  if (!raw) continue;
15635
- const entry = toPlain(raw);
15824
+ const entry = toPlain$1(raw);
15636
15825
  trashMap.set(nid, {
15637
15826
  label: entry.label || "Untitled",
15638
15827
  parentId: entry.parentId ?? null,
@@ -15663,14 +15852,11 @@ function registerTreeTools(mcp, server, validator = null) {
15663
15852
  type: "text",
15664
15853
  text: "Not connected"
15665
15854
  }] };
15666
- const raw = treeMap.get(id);
15667
- if (!raw) return { content: [{
15855
+ if (!treeMap.get(id)) return { content: [{
15668
15856
  type: "text",
15669
15857
  text: `Document ${id} not found`
15670
15858
  }] };
15671
- const entry = toPlain(raw);
15672
- treeMap.set(id, {
15673
- ...entry,
15859
+ patchEntry(treeMap, id, {
15674
15860
  type,
15675
15861
  updatedAt: Date.now()
15676
15862
  });
@@ -15713,13 +15899,13 @@ function registerTreeTools(mcp, server, validator = null) {
15713
15899
  type: "text",
15714
15900
  text: `Document ${id} not found`
15715
15901
  }] };
15716
- const entry = toPlain(raw);
15902
+ const entry = toPlain$1(raw);
15717
15903
  const newId = crypto.randomUUID();
15718
- treeMap.set(newId, {
15904
+ treeMap.set(newId, makeEntryMap({
15719
15905
  ...entry,
15720
15906
  label: (entry.label || "Untitled") + " (copy)",
15721
15907
  order: Date.now()
15722
- });
15908
+ }));
15723
15909
  server.setFocusedDoc(newId);
15724
15910
  return { content: [{
15725
15911
  type: "text",
@@ -15762,31 +15948,37 @@ function registerContentTools(mcp, server) {
15762
15948
  name: "read_document",
15763
15949
  target: docId
15764
15950
  });
15765
- const { title, markdown } = yjsToMarkdown((await server.getChildProvider(docId)).document.getXmlFragment("default"));
15951
+ const fragment = (await server.getChildProvider(docId)).document.getXmlFragment("default");
15766
15952
  server.setFocusedDoc(docId);
15767
15953
  server.setDocCursor(docId, 0);
15768
15954
  const treeMap = server.getTreeMap();
15769
- let label = title;
15955
+ let label = "Untitled";
15770
15956
  let type;
15771
15957
  let meta;
15772
15958
  let children = [];
15773
15959
  if (treeMap) {
15774
- const entry = treeMap.get(docId);
15775
- if (entry) {
15776
- label = entry.label || title;
15960
+ const entry = toPlain(treeMap.get(docId));
15961
+ if (entry && typeof entry === "object") {
15962
+ label = entry.label || label;
15777
15963
  type = entry.type;
15778
15964
  meta = entry.meta;
15779
15965
  }
15780
- treeMap.forEach((value, id) => {
15781
- if (value.parentId === docId) children.push({
15966
+ const collected = [];
15967
+ treeMap.forEach((raw, id) => {
15968
+ const value = toPlain(raw);
15969
+ if (typeof value !== "object" || value === null) return;
15970
+ if (value.parentId === docId) collected.push({
15782
15971
  id,
15783
15972
  label: value.label || "Untitled",
15784
15973
  type: value.type,
15785
- meta: value.meta
15974
+ meta: value.meta,
15975
+ order: value.order ?? 0
15786
15976
  });
15787
15977
  });
15788
- children.sort((a, b) => (treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0));
15978
+ collected.sort((a, b) => a.order - b.order);
15979
+ children = collected.map(({ order, ...rest }) => rest);
15789
15980
  }
15981
+ const markdown = yjsToMarkdown(fragment, label, meta, type);
15790
15982
  const result = {
15791
15983
  label,
15792
15984
  type,
@@ -15827,19 +16019,19 @@ function registerContentTools(mcp, server) {
15827
16019
  const treeMap = server.getTreeMap();
15828
16020
  const rootDoc = server.rootDocument;
15829
16021
  if (treeMap && rootDoc) {
15830
- const entry = treeMap.get(docId);
15831
- if (entry) rootDoc.transact(() => {
15832
- const updates = {
15833
- ...entry,
15834
- updatedAt: Date.now()
15835
- };
15836
- if (title) updates.label = title;
15837
- if (Object.keys(meta).length > 0) updates.meta = {
15838
- ...entry.meta ?? {},
15839
- ...meta
15840
- };
15841
- treeMap.set(docId, updates);
15842
- });
16022
+ const rawEntry = treeMap.get(docId);
16023
+ if (rawEntry) {
16024
+ const cur = toPlain(rawEntry);
16025
+ rootDoc.transact(() => {
16026
+ const patch = { updatedAt: Date.now() };
16027
+ if (title) patch.label = title;
16028
+ if (Object.keys(meta).length > 0) patch.meta = {
16029
+ ...cur.meta ?? {},
16030
+ ...meta
16031
+ };
16032
+ patchEntry(treeMap, docId, patch);
16033
+ });
16034
+ }
15843
16035
  }
15844
16036
  }
15845
16037
  if (writeMode === "replace") doc.transact(() => {
@@ -15907,11 +16099,12 @@ function registerMetaTools(mcp, server, validator = null) {
15907
16099
  type: "text",
15908
16100
  text: "Not connected"
15909
16101
  }] };
15910
- const entry = treeMap.get(docId);
15911
- if (!entry) return { content: [{
16102
+ const rawEntry = treeMap.get(docId);
16103
+ if (!rawEntry) return { content: [{
15912
16104
  type: "text",
15913
16105
  text: `Document ${docId} not found`
15914
16106
  }] };
16107
+ const entry = toPlain(rawEntry);
15915
16108
  const mergedMeta = {
15916
16109
  ...entry.meta ?? {},
15917
16110
  ...meta
@@ -15929,8 +16122,7 @@ function registerMetaTools(mcp, server, validator = null) {
15929
16122
  };
15930
16123
  }
15931
16124
  }
15932
- treeMap.set(docId, {
15933
- ...entry,
16125
+ patchEntry(treeMap, docId, {
15934
16126
  meta: mergedMeta,
15935
16127
  updatedAt: Date.now()
15936
16128
  });
@@ -16120,7 +16312,7 @@ function registerAwarenessTools(mcp, server) {
16120
16312
  })
16121
16313
  }] };
16122
16314
  const resolvedInboxId = inboxId;
16123
- const { markdown } = yjsToMarkdown((await server.getChildProvider(resolvedInboxId)).document.getXmlFragment("default"));
16315
+ const markdown = yjsToMarkdown((await server.getChildProvider(resolvedInboxId)).document.getXmlFragment("default"), "AI Inbox");
16124
16316
  const pendingTasks = [];
16125
16317
  treeMap.forEach((value, id) => {
16126
16318
  if (value.parentId === resolvedInboxId) pendingTasks.push({
@@ -16206,16 +16398,16 @@ function registerChannelTools(mcp, server) {
16206
16398
  const replyId = crypto.randomUUID();
16207
16399
  const now = Date.now();
16208
16400
  rootDoc.transact(() => {
16209
- treeMap.set(replyId, {
16401
+ treeMap.set(replyId, makeEntryMap({
16210
16402
  label,
16211
16403
  parentId: doc_id,
16212
16404
  order: now,
16213
16405
  type: "doc",
16214
16406
  createdAt: now,
16215
16407
  updatedAt: now
16216
- });
16408
+ }));
16217
16409
  });
16218
- populateYDocFromMarkdown((await server.getChildProvider(replyId)).document, text);
16410
+ populateYDocFromMarkdown((await server.getChildProvider(replyId)).document.getXmlFragment("default"), text);
16219
16411
  if (task_id) server.clearAiTask(task_id);
16220
16412
  return { content: [{
16221
16413
  type: "text",
@@ -16989,9 +17181,14 @@ function registerAgentGuide(mcp) {
16989
17181
 
16990
17182
  //#endregion
16991
17183
  //#region packages/mcp/src/resources/tree-resource.ts
17184
+ /**
17185
+ * Dynamic resource: document tree as navigable JSON.
17186
+ */
16992
17187
  function readEntries(treeMap) {
16993
17188
  const entries = [];
16994
- treeMap.forEach((value, id) => {
17189
+ treeMap.forEach((raw, id) => {
17190
+ const value = toPlain(raw);
17191
+ if (typeof value !== "object" || value === null) return;
16995
17192
  entries.push({
16996
17193
  id,
16997
17194
  label: value.label || "Untitled",