mindoodb-app-sdk 0.0.63 → 0.0.65

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.
@@ -102,7 +102,7 @@ function getCategoryPath(entry) {
102
102
  }
103
103
  return path;
104
104
  }
105
- function serializeEntry(navigator, entry) {
105
+ function serializeEntry(navigator2, entry) {
106
106
  const parent = entry.getParent();
107
107
  return {
108
108
  key: entry.getPositionStr(),
@@ -122,8 +122,8 @@ function serializeEntry(navigator, entry) {
122
122
  childCategoryCount: entry.getChildCategoryCount(),
123
123
  childDocumentCount: entry.getChildDocumentCount(),
124
124
  position: entry.getPositionStr(),
125
- expanded: entry.isCategory() ? navigator.isExpanded(entry) : false,
126
- selected: navigator.isSelected(entry.origin, entry.docId),
125
+ expanded: entry.isCategory() ? navigator2.isExpanded(entry) : false,
126
+ selected: navigator2.isSelected(entry.origin, entry.docId),
127
127
  isVisible: true
128
128
  };
129
129
  }
@@ -173,24 +173,24 @@ async function buildVirtualView(mindoodb, definition, documents, categorizationS
173
173
  break;
174
174
  }
175
175
  }
176
- const view = await builder.build();
176
+ const view2 = await builder.build();
177
177
  const changes = /* @__PURE__ */ new Map();
178
178
  for (const document2 of documents) {
179
179
  if (!matchesFilter(mindoodb, definition, document2)) {
180
180
  continue;
181
181
  }
182
- const change = changes.get(document2.origin) ?? new mindoodb.VirtualViewDataChange(document2.origin);
183
- change.addEntry(document2.docId, computeColumnValues(definition, document2));
184
- changes.set(document2.origin, change);
182
+ const change2 = changes.get(document2.origin) ?? new mindoodb.VirtualViewDataChange(document2.origin);
183
+ change2.addEntry(document2.docId, computeColumnValues(definition, document2));
184
+ changes.set(document2.origin, change2);
185
185
  }
186
- for (const change of changes.values()) {
187
- if (change.hasChanges()) view.applyChanges(change);
186
+ for (const change2 of changes.values()) {
187
+ if (change2.hasChanges()) view2.applyChanges(change2);
188
188
  }
189
- view.getRoot();
190
- return view;
189
+ view2.getRoot();
190
+ return view2;
191
191
  }
192
- function createCoreNavigator(mindoodb, view, options) {
193
- const builder = mindoodb.VirtualViewFactory.createNavigator(view);
192
+ function createCoreNavigator(mindoodb, view2, options) {
193
+ const builder = mindoodb.VirtualViewFactory.createNavigator(view2);
194
194
  const includeCategories = options?.includeCategories ?? true;
195
195
  const includeDocuments = options?.includeDocuments ?? true;
196
196
  if (!includeCategories && includeDocuments) builder.documentsOnly();
@@ -199,7 +199,7 @@ function createCoreNavigator(mindoodb, view, options) {
199
199
  if (Array.isArray(options?.rootCategoryPath) && options.rootCategoryPath.length > 0) {
200
200
  builder.fromCategory(options.rootCategoryPath.map((part) => String(part)).join("\\"));
201
201
  } else if (options?.rootEntryKey) {
202
- const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(view);
202
+ const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(view2);
203
203
  const rootEntry = temp.getPos(options.rootEntryKey);
204
204
  if (rootEntry) builder.fromEntry(rootEntry);
205
205
  }
@@ -208,21 +208,21 @@ function createCoreNavigator(mindoodb, view, options) {
208
208
  function normalizePageLimit(limit) {
209
209
  return Math.max(1, Math.min(1e3, Math.trunc(limit ?? 100)));
210
210
  }
211
- function pageEntries(navigator, direction, options = {}) {
211
+ function pageEntries(navigator2, direction, options = {}) {
212
212
  const entries = [];
213
- const startOk = options.startPosition ? navigator.gotoPos(options.startPosition) : direction === "forward" ? navigator.gotoFirst() : navigator.gotoLast();
213
+ const startOk = options.startPosition ? navigator2.gotoPos(options.startPosition) : direction === "forward" ? navigator2.gotoFirst() : navigator2.gotoLast();
214
214
  if (!startOk) {
215
215
  return { entries, nextPosition: null, hasMore: false };
216
216
  }
217
217
  const moveNext = () => {
218
218
  if (direction === "forward") {
219
- return options.selectedOnly ? navigator.gotoNextSelected() : navigator.gotoNext();
219
+ return options.selectedOnly ? navigator2.gotoNextSelected() : navigator2.gotoNext();
220
220
  }
221
- return options.selectedOnly ? navigator.gotoPrevSelected() : navigator.gotoPrev();
221
+ return options.selectedOnly ? navigator2.gotoPrevSelected() : navigator2.gotoPrev();
222
222
  };
223
223
  if (options.selectedOnly) {
224
- const current = navigator.getCurrentEntry();
225
- if (!current || !navigator.isSelected(current.origin, current.docId)) {
224
+ const current = navigator2.getCurrentEntry();
225
+ if (!current || !navigator2.isSelected(current.origin, current.docId)) {
226
226
  if (!moveNext()) return { entries, nextPosition: null, hasMore: false };
227
227
  }
228
228
  }
@@ -230,26 +230,26 @@ function pageEntries(navigator, direction, options = {}) {
230
230
  let nextPosition = null;
231
231
  let hasMore = false;
232
232
  while (true) {
233
- const current = navigator.getCurrentEntry();
233
+ const current = navigator2.getCurrentEntry();
234
234
  if (!current) break;
235
- entries.push(serializeEntry(navigator, current));
235
+ entries.push(serializeEntry(navigator2, current));
236
236
  if (entries.length >= limit) {
237
237
  hasMore = moveNext();
238
- nextPosition = hasMore ? navigator.getCurrentEntry()?.getPositionStr() ?? null : null;
238
+ nextPosition = hasMore ? navigator2.getCurrentEntry()?.getPositionStr() ?? null : null;
239
239
  break;
240
240
  }
241
241
  if (!moveNext()) break;
242
242
  }
243
243
  return { entries, nextPosition, hasMore };
244
244
  }
245
- function resolveEntry(navigator, entryKey) {
246
- const entry = navigator.getPos(entryKey);
245
+ function resolveEntry(navigator2, entryKey) {
246
+ const entry = navigator2.getPos(entryKey);
247
247
  if (!entry) {
248
248
  throw new Error(`Unknown view entry: ${entryKey}`);
249
249
  }
250
250
  return entry;
251
251
  }
252
- function wrapNavigator(definition, navigator) {
252
+ function wrapNavigator(definition, navigator2) {
253
253
  const updateListeners = /* @__PURE__ */ new Set();
254
254
  let viewCursor = null;
255
255
  return {
@@ -267,164 +267,164 @@ function wrapNavigator(definition, navigator) {
267
267
  return () => updateListeners.delete(listener);
268
268
  },
269
269
  async getCurrentEntry() {
270
- const entry = navigator.getCurrentEntry();
271
- return entry ? serializeEntry(navigator, entry) : null;
270
+ const entry = navigator2.getCurrentEntry();
271
+ return entry ? serializeEntry(navigator2, entry) : null;
272
272
  },
273
273
  async gotoFirst() {
274
- return navigator.gotoFirst();
274
+ return navigator2.gotoFirst();
275
275
  },
276
276
  async gotoLast() {
277
- return navigator.gotoLast();
277
+ return navigator2.gotoLast();
278
278
  },
279
279
  async gotoNext() {
280
- return navigator.gotoNext();
280
+ return navigator2.gotoNext();
281
281
  },
282
282
  async gotoPrev() {
283
- return navigator.gotoPrev();
283
+ return navigator2.gotoPrev();
284
284
  },
285
285
  async gotoNextSibling() {
286
- return navigator.gotoNextSibling();
286
+ return navigator2.gotoNextSibling();
287
287
  },
288
288
  async gotoPrevSibling() {
289
- return navigator.gotoPrevSibling();
289
+ return navigator2.gotoPrevSibling();
290
290
  },
291
291
  async gotoParent() {
292
- return navigator.gotoParent();
292
+ return navigator2.gotoParent();
293
293
  },
294
294
  async gotoFirstChild() {
295
- return navigator.gotoFirstChild();
295
+ return navigator2.gotoFirstChild();
296
296
  },
297
297
  async gotoLastChild() {
298
- return navigator.gotoLastChild();
298
+ return navigator2.gotoLastChild();
299
299
  },
300
300
  async gotoPos(position) {
301
- return navigator.gotoPos(position);
301
+ return navigator2.gotoPos(position);
302
302
  },
303
303
  async getPos(position) {
304
- const entry = navigator.getPos(position);
305
- return entry ? serializeEntry(navigator, entry) : null;
304
+ const entry = navigator2.getPos(position);
305
+ return entry ? serializeEntry(navigator2, entry) : null;
306
306
  },
307
307
  async findCategoryEntryByParts(parts) {
308
- const entry = navigator.findCategoryEntryByParts(parts);
309
- return entry ? serializeEntry(navigator, entry) : null;
308
+ const entry = navigator2.findCategoryEntryByParts(parts);
309
+ return entry ? serializeEntry(navigator2, entry) : null;
310
310
  },
311
311
  async entriesForward(options) {
312
- return pageEntries(navigator, "forward", options);
312
+ return pageEntries(navigator2, "forward", options);
313
313
  },
314
314
  async entriesBackward(options) {
315
- return pageEntries(navigator, "backward", options);
315
+ return pageEntries(navigator2, "backward", options);
316
316
  },
317
317
  async gotoNextSelected() {
318
- return navigator.gotoNextSelected();
318
+ return navigator2.gotoNextSelected();
319
319
  },
320
320
  async gotoPrevSelected() {
321
- return navigator.gotoPrevSelected();
321
+ return navigator2.gotoPrevSelected();
322
322
  },
323
323
  async select(origin, docId, selectParentCategories = false) {
324
- navigator.select(origin, docId, selectParentCategories);
324
+ navigator2.select(origin, docId, selectParentCategories);
325
325
  },
326
326
  async deselect(origin, docId) {
327
- navigator.deselect(origin, docId);
327
+ navigator2.deselect(origin, docId);
328
328
  },
329
329
  async selectAllEntries() {
330
- navigator.selectAllEntries();
330
+ navigator2.selectAllEntries();
331
331
  },
332
332
  async deselectAllEntries() {
333
- navigator.deselectAllEntries();
333
+ navigator2.deselectAllEntries();
334
334
  },
335
335
  async isSelected(origin, docId) {
336
- return navigator.isSelected(origin, docId);
336
+ return navigator2.isSelected(origin, docId);
337
337
  },
338
338
  async getSelectionState() {
339
339
  return {
340
- selectAllByDefault: navigator.isSelectAllByDefault(),
341
- entryKeys: Array.from(navigator.getSelectedOrDeselectedEntries())
340
+ selectAllByDefault: navigator2.isSelectAllByDefault(),
341
+ entryKeys: Array.from(navigator2.getSelectedOrDeselectedEntries())
342
342
  };
343
343
  },
344
344
  async setSelectionState(state) {
345
- if (state.selectAllByDefault) navigator.selectAllEntries();
346
- else navigator.deselectAllEntries();
347
- navigator.setSelectedOrDeselectedEntries(new Set(state.entryKeys));
345
+ if (state.selectAllByDefault) navigator2.selectAllEntries();
346
+ else navigator2.deselectAllEntries();
347
+ navigator2.setSelectedOrDeselectedEntries(new Set(state.entryKeys));
348
348
  },
349
349
  async expand(origin, docId) {
350
- navigator.expand(origin, docId);
350
+ navigator2.expand(origin, docId);
351
351
  },
352
352
  async collapse(origin, docId) {
353
- navigator.collapse(origin, docId);
353
+ navigator2.collapse(origin, docId);
354
354
  },
355
355
  async expandAll() {
356
- navigator.expandAll();
356
+ navigator2.expandAll();
357
357
  },
358
358
  async collapseAll() {
359
- navigator.collapseAll();
359
+ navigator2.collapseAll();
360
360
  },
361
361
  async expandToLevel(level) {
362
- navigator.expandToLevel(level);
362
+ navigator2.expandToLevel(level);
363
363
  },
364
364
  async isExpanded(entryKey) {
365
- const entry = navigator.getPos(entryKey);
366
- return entry ? navigator.isExpanded(entry) : false;
365
+ const entry = navigator2.getPos(entryKey);
366
+ return entry ? navigator2.isExpanded(entry) : false;
367
367
  },
368
368
  async getExpansionState() {
369
369
  return {
370
- expandAllByDefault: navigator.isExpandAllByDefault(),
371
- expandLevel: navigator.getExpandLevel(),
372
- entryKeys: Array.from(navigator.getExpandedOrCollapsedEntries())
370
+ expandAllByDefault: navigator2.isExpandAllByDefault(),
371
+ expandLevel: navigator2.getExpandLevel(),
372
+ entryKeys: Array.from(navigator2.getExpandedOrCollapsedEntries())
373
373
  };
374
374
  },
375
375
  async setExpansionState(state) {
376
- if (state.expandAllByDefault) navigator.expandAll();
377
- else navigator.collapseAll();
378
- navigator.expandToLevel(state.expandLevel);
379
- navigator.setExpandedOrCollapsedEntries(new Set(state.entryKeys));
376
+ if (state.expandAllByDefault) navigator2.expandAll();
377
+ else navigator2.collapseAll();
378
+ navigator2.expandToLevel(state.expandLevel);
379
+ navigator2.setExpandedOrCollapsedEntries(new Set(state.entryKeys));
380
380
  },
381
381
  async childEntries(entryKey, descending) {
382
- return navigator.childEntries(resolveEntry(navigator, entryKey), descending).map((entry) => serializeEntry(navigator, entry));
382
+ return navigator2.childEntries(resolveEntry(navigator2, entryKey), descending).map((entry) => serializeEntry(navigator2, entry));
383
383
  },
384
384
  async childCategories(entryKey, descending) {
385
- return navigator.childCategories(resolveEntry(navigator, entryKey), descending).map((entry) => serializeEntry(navigator, entry));
385
+ return navigator2.childCategories(resolveEntry(navigator2, entryKey), descending).map((entry) => serializeEntry(navigator2, entry));
386
386
  },
387
387
  async childDocuments(entryKey, descending) {
388
- return navigator.childDocuments(resolveEntry(navigator, entryKey), descending).map((entry) => serializeEntry(navigator, entry));
388
+ return navigator2.childDocuments(resolveEntry(navigator2, entryKey), descending).map((entry) => serializeEntry(navigator2, entry));
389
389
  },
390
390
  async childCategoriesByKey(entryKey, key, exact, descending) {
391
- return navigator.childCategoriesByKey(
392
- resolveEntry(navigator, entryKey),
391
+ return navigator2.childCategoriesByKey(
392
+ resolveEntry(navigator2, entryKey),
393
393
  String(key ?? ""),
394
394
  Boolean(exact),
395
395
  descending
396
- ).map((entry) => serializeEntry(navigator, entry));
396
+ ).map((entry) => serializeEntry(navigator2, entry));
397
397
  },
398
398
  async childDocumentsByKey(entryKey, key, exact, descending) {
399
- return navigator.childDocumentsByKey(
400
- resolveEntry(navigator, entryKey),
399
+ return navigator2.childDocumentsByKey(
400
+ resolveEntry(navigator2, entryKey),
401
401
  String(key ?? ""),
402
402
  Boolean(exact),
403
403
  descending
404
- ).map((entry) => serializeEntry(navigator, entry));
404
+ ).map((entry) => serializeEntry(navigator2, entry));
405
405
  },
406
406
  async childCategoriesBetween(entryKey, range) {
407
- return navigator.childCategoriesBetween(
408
- resolveEntry(navigator, entryKey),
407
+ return navigator2.childCategoriesBetween(
408
+ resolveEntry(navigator2, entryKey),
409
409
  range.startKey,
410
410
  range.endKey,
411
411
  range.descending
412
- ).map((entry) => serializeEntry(navigator, entry));
412
+ ).map((entry) => serializeEntry(navigator2, entry));
413
413
  },
414
414
  async childDocumentsBetween(entryKey, range) {
415
- return navigator.childDocumentsBetween(
416
- resolveEntry(navigator, entryKey),
415
+ return navigator2.childDocumentsBetween(
416
+ resolveEntry(navigator2, entryKey),
417
417
  range.startKey,
418
418
  range.endKey,
419
419
  range.descending
420
- ).map((entry) => serializeEntry(navigator, entry));
420
+ ).map((entry) => serializeEntry(navigator2, entry));
421
421
  },
422
422
  async getSortedDocIds(descending) {
423
- const page = pageEntries(navigator, descending ? "backward" : "forward", { limit: 1e3 });
423
+ const page = pageEntries(navigator2, descending ? "backward" : "forward", { limit: 1e3 });
424
424
  return collectSortedDocIds(page.entries);
425
425
  },
426
426
  async getSortedDocIdsScoped(entryKey, descending) {
427
- const docs = navigator.childDocuments(resolveEntry(navigator, entryKey), descending);
427
+ const docs = navigator2.childDocuments(resolveEntry(navigator2, entryKey), descending);
428
428
  return docs.map((entry) => ({ origin: entry.origin, docId: entry.docId }));
429
429
  },
430
430
  async dispose() {
@@ -447,14 +447,14 @@ function collectSortedDocIds(entries) {
447
447
  async function createEvaluatingViewNavigator(input, documents) {
448
448
  const mindoodb = await loadMindoodbForTesting();
449
449
  if (!mindoodb) return null;
450
- const view = await buildVirtualView(
450
+ const view2 = await buildVirtualView(
451
451
  mindoodb,
452
452
  input.definition,
453
453
  documents,
454
454
  input.categorizationStyle
455
455
  );
456
- const navigator = createCoreNavigator(mindoodb, view, input.options);
457
- return wrapNavigator(input.definition, navigator);
456
+ const navigator2 = createCoreNavigator(mindoodb, view2, input.options);
457
+ return wrapNavigator(input.definition, navigator2);
458
458
  }
459
459
 
460
460
  // src/testing/mockEmbeds.ts
@@ -828,6 +828,246 @@ function guardDatabase(database, currentInfo, enforce) {
828
828
  };
829
829
  }
830
830
 
831
+ // src/testing/mockAgentTools.ts
832
+ var TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]{0,47}$/;
833
+ var PREFIX_PATTERN = /^[a-z][a-z0-9_]{0,15}$/;
834
+ var MOCK_AGENT_MAX_TOOLS = 64;
835
+ var MOCK_AGENT_MAX_DESCRIPTION = 2e3;
836
+ var MOCK_AGENT_FILE_TTL_MS = 10 * 60 * 1e3;
837
+ var MOCK_AGENT_FILE_MAX_BYTES = 50 * 1024 * 1024;
838
+ var DEFAULT_INVOKE_TIMEOUT_MS = 12e4;
839
+ function invalid(message) {
840
+ return Object.assign(new Error(message), { name: "invalid-params" });
841
+ }
842
+ function notFound(message) {
843
+ return Object.assign(new Error(message), { name: "not-found" });
844
+ }
845
+ function mockAgentToolPrefix(appId, declared) {
846
+ if (declared && PREFIX_PATTERN.test(declared) && !declared.endsWith("_") && !/^haven(?:_|$)/.test(declared)) {
847
+ return declared;
848
+ }
849
+ const key = (appId.split("\\").pop() ?? "").trim().toLowerCase().replace(/^mindoodb-app-/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 16).replace(/_+$/, "");
850
+ if (!key) return "app";
851
+ return /^[a-z]/.test(key) ? key : `app_${key}`.slice(0, 16);
852
+ }
853
+ function validateMockAgentTools(raw) {
854
+ if (!Array.isArray(raw)) throw invalid("tools must be an array.");
855
+ if (raw.length > MOCK_AGENT_MAX_TOOLS) throw invalid(`An app may offer at most ${MOCK_AGENT_MAX_TOOLS} agent tools.`);
856
+ const seen = /* @__PURE__ */ new Set();
857
+ return raw.map((entry) => {
858
+ const tool = entry;
859
+ if (!tool || typeof tool.name !== "string" || !TOOL_NAME_PATTERN.test(tool.name)) {
860
+ throw invalid(`Invalid agent tool name "${String(tool?.name)}".`);
861
+ }
862
+ if (seen.has(tool.name)) throw invalid(`Agent tool "${tool.name}" is declared twice.`);
863
+ seen.add(tool.name);
864
+ if (typeof tool.description !== "string" || !tool.description.trim()) {
865
+ throw invalid(`Agent tool "${tool.name}" needs a description.`);
866
+ }
867
+ if (tool.description.length > MOCK_AGENT_MAX_DESCRIPTION) {
868
+ throw invalid(`The description of agent tool "${tool.name}" is too long.`);
869
+ }
870
+ const schema = tool.inputSchema;
871
+ if (!schema || typeof schema !== "object" || Array.isArray(schema) || schema.type !== "object") {
872
+ throw invalid(`Agent tool "${tool.name}" needs an object inputSchema.`);
873
+ }
874
+ const annotations = tool.annotations ?? {};
875
+ for (const key of ["readOnlyHint", "consequentialHint", "untrustedContentHint"]) {
876
+ if (annotations[key] !== void 0 && typeof annotations[key] !== "boolean") {
877
+ throw invalid(`Agent tool "${tool.name}" has invalid annotations.`);
878
+ }
879
+ }
880
+ return {
881
+ name: tool.name,
882
+ description: tool.description,
883
+ inputSchema: schema,
884
+ ...tool.annotations ? { annotations: { ...tool.annotations } } : {}
885
+ };
886
+ });
887
+ }
888
+ async function bytesOf(data) {
889
+ if (typeof data === "string") return new TextEncoder().encode(data);
890
+ if (data instanceof Uint8Array) return data.slice();
891
+ if (typeof Blob !== "undefined" && data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
892
+ return new Uint8Array(data);
893
+ }
894
+ function structuralBytes(data) {
895
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
896
+ if (Object.prototype.toString.call(data) === "[object ArrayBuffer]") return new Uint8Array(data).slice();
897
+ return null;
898
+ }
899
+ function createMockAgentHost(options) {
900
+ const prefix = mockAgentToolPrefix(options.appId(), options.agentToolPrefix);
901
+ let tools = [];
902
+ let context = null;
903
+ let enabled = options.agentToolsEnabled ?? true;
904
+ let consent = options.agentConsent ?? "allow";
905
+ const timeoutMs = options.agentInvokeTimeoutMs ?? DEFAULT_INVOKE_TIMEOUT_MS;
906
+ const files = /* @__PURE__ */ new Map();
907
+ const calls = [];
908
+ const pending = /* @__PURE__ */ new Map();
909
+ let counter = 0;
910
+ const changed = () => options.onChange?.();
911
+ function prune(now = Date.now()) {
912
+ for (const [ref, file] of files) if (file.expiresAt <= now) files.delete(ref);
913
+ }
914
+ function addFile(file) {
915
+ prune();
916
+ if (file.data.length > MOCK_AGENT_FILE_MAX_BYTES) {
917
+ throw invalid(`Files for agents may be at most ${MOCK_AGENT_FILE_MAX_BYTES / 1024 / 1024} MB.`);
918
+ }
919
+ const now = Date.now();
920
+ counter += 1;
921
+ const entry = {
922
+ ...file,
923
+ fileRef: `file_test${counter.toString(16).padStart(4, "0")}`,
924
+ createdAt: now,
925
+ expiresAt: now + MOCK_AGENT_FILE_TTL_MS
926
+ };
927
+ files.set(entry.fileRef, entry);
928
+ changed();
929
+ return entry;
930
+ }
931
+ function registration() {
932
+ return { enabled, exposedNames: tools.map((tool) => `${prefix}_${tool.name}`) };
933
+ }
934
+ async function dispatch(method, params) {
935
+ const input = params ?? {};
936
+ if (method === "agent.registerTools") {
937
+ tools = validateMockAgentTools(input.tools);
938
+ changed();
939
+ return { result: registration() };
940
+ }
941
+ if (method === "agent.setContext") {
942
+ const next = input.context ?? null;
943
+ if (next !== null && (typeof next !== "object" || Array.isArray(next))) {
944
+ throw invalid("context must be an object or null.");
945
+ }
946
+ context = next;
947
+ changed();
948
+ return { result: null };
949
+ }
950
+ if (method === "agent.provideFile") {
951
+ if (typeof input.name !== "string" || !input.name.trim()) throw invalid("provideFile needs a file name.");
952
+ const data = structuralBytes(input.data);
953
+ if (!data) throw invalid("provideFile needs the file content as an ArrayBuffer.");
954
+ const mimeType = typeof input.mimeType === "string" && input.mimeType ? input.mimeType : "application/octet-stream";
955
+ const file = addFile({ name: input.name.trim(), mimeType, data, source: "app" });
956
+ return { result: { fileRef: file.fileRef, size: file.data.length } };
957
+ }
958
+ if (method === "agent.takeFile") {
959
+ prune();
960
+ const file = typeof input.fileRef === "string" ? files.get(input.fileRef) : void 0;
961
+ if (!file || file.source !== "host") throw notFound("Unknown or expired file reference.");
962
+ return { result: { name: file.name, mimeType: file.mimeType, data: file.data.slice().buffer } };
963
+ }
964
+ return void 0;
965
+ }
966
+ function finish(callId, outcome) {
967
+ const call2 = calls.find((entry) => entry.callId === callId);
968
+ if (call2) call2.outcome = outcome;
969
+ changed();
970
+ return outcome;
971
+ }
972
+ async function call(name, input = {}) {
973
+ counter += 1;
974
+ const callId = `test-call-${counter}`;
975
+ const startedAt = Date.now();
976
+ const toolName = name.startsWith(`${prefix}_`) && !tools.some((t) => t.name === name) ? name.slice(prefix.length + 1) : name;
977
+ calls.push({ callId, tool: toolName, input, startedAt });
978
+ changed();
979
+ const failWith = (code, message, requiredAction) => finish(callId, {
980
+ ok: false,
981
+ error: { code, message, ...requiredAction ? { requiredAction } : {} },
982
+ durationMs: Date.now() - startedAt
983
+ });
984
+ if (!enabled) return failWith("NOT_ALLOWED", "The user has agent tools switched off for this app.");
985
+ const tool = tools.find((entry) => entry.name === toolName);
986
+ if (!tool) return failWith("NOT_FOUND", `The app offers no tool "${name}".`);
987
+ if (tool.annotations?.consequentialHint) {
988
+ const allowed = typeof consent === "function" ? await consent({ tool: toolName, input }) : consent === "allow";
989
+ if (!allowed) return failWith("NOT_ALLOWED", "The user declined this call.", "ask the user before trying again");
990
+ }
991
+ return await new Promise((resolve) => {
992
+ const timer = setTimeout(() => {
993
+ pending.delete(callId);
994
+ resolve(failWith("FAILED", `The app did not answer "${toolName}" in time.`));
995
+ }, timeoutMs);
996
+ pending.set(callId, { resolve, startedAt, timer });
997
+ const posted = options.post({ protocol: "mindoodb-app-bridge", kind: "agent-invoke", callId, tool: toolName, input });
998
+ if (!posted) {
999
+ clearTimeout(timer);
1000
+ pending.delete(callId);
1001
+ resolve(failWith("INVALID_STATE", "The app is not connected."));
1002
+ }
1003
+ });
1004
+ }
1005
+ function handleResult(message) {
1006
+ const entry = pending.get(message.callId);
1007
+ if (!entry) return;
1008
+ pending.delete(message.callId);
1009
+ clearTimeout(entry.timer);
1010
+ const durationMs = Date.now() - entry.startedAt;
1011
+ entry.resolve(
1012
+ finish(
1013
+ message.callId,
1014
+ message.ok ? { ok: true, result: message.result ?? null, durationMs } : {
1015
+ ok: false,
1016
+ error: {
1017
+ code: message.error?.code ?? "FAILED",
1018
+ message: message.error?.message ?? "The app reported an error.",
1019
+ ...message.error?.requiredAction ? { requiredAction: message.error.requiredAction } : {}
1020
+ },
1021
+ durationMs
1022
+ }
1023
+ )
1024
+ );
1025
+ }
1026
+ function reset(reason = "The app was closed.") {
1027
+ for (const [callId, entry] of pending) {
1028
+ clearTimeout(entry.timer);
1029
+ entry.resolve(
1030
+ finish(callId, { ok: false, error: { code: "INVALID_STATE", message: reason }, durationMs: Date.now() - entry.startedAt })
1031
+ );
1032
+ }
1033
+ pending.clear();
1034
+ tools = [];
1035
+ context = null;
1036
+ changed();
1037
+ }
1038
+ const controller = {
1039
+ prefix,
1040
+ tools: () => tools.map((tool) => ({ ...tool })),
1041
+ exposedNames: () => registration().exposedNames,
1042
+ context: () => context,
1043
+ isEnabled: () => enabled,
1044
+ setEnabled(next) {
1045
+ enabled = next;
1046
+ changed();
1047
+ },
1048
+ setConsent(next) {
1049
+ consent = next;
1050
+ },
1051
+ call,
1052
+ calls: () => calls,
1053
+ async importFile(file) {
1054
+ const data = await bytesOf(file.data);
1055
+ const mimeType = file.mimeType || (typeof Blob !== "undefined" && file.data instanceof Blob ? file.data.type : "") || "application/octet-stream";
1056
+ return addFile({ name: file.name, mimeType, data, source: "host" }).fileRef;
1057
+ },
1058
+ getFile(fileRef) {
1059
+ prune();
1060
+ const file = files.get(fileRef);
1061
+ return file && file.source === "app" ? file : null;
1062
+ },
1063
+ files() {
1064
+ prune();
1065
+ return [...files.values()].sort((a, b) => b.createdAt - a.createdAt);
1066
+ }
1067
+ };
1068
+ return { controller, dispatch, handleResult, reset };
1069
+ }
1070
+
831
1071
  // src/testing/memoryAttachments.ts
832
1072
  function concat(chunks) {
833
1073
  const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
@@ -930,6 +1170,7 @@ function mockDatabasesFromDefinition(definition, seed = {}, options = {}) {
930
1170
  capabilities: override?.capabilities ? [...new Set(override.capabilities)] : capabilitiesForDefinitionPermissions(database.permissions)
931
1171
  },
932
1172
  documents: seed[database.logicalDatabaseId],
1173
+ ...options.automerge ? { automerge: true } : {},
933
1174
  ...options.attachments === "none" ? {} : { methods: { attachments: createMemoryAttachments() } }
934
1175
  };
935
1176
  });
@@ -950,6 +1191,8 @@ function readTestHostUrlSettings(search) {
950
1191
  }
951
1192
  const enforce = params.get("enforce");
952
1193
  if (enforce !== null) settings.enforceCapabilities = enforce === "1" || enforce === "true";
1194
+ const automerge = params.get("automerge");
1195
+ if (automerge !== null) settings.automerge = automerge === "1" || automerge === "true";
953
1196
  const users = Number.parseInt(params.get("users") ?? "", 10);
954
1197
  if (Number.isFinite(users) && users >= 0) settings.directoryUserCount = users;
955
1198
  const locale = params.get("locale");
@@ -1020,6 +1263,10 @@ function createBrowserTestHost(options) {
1020
1263
  const changed = () => queueMicrotask(() => onChange?.());
1021
1264
  const host = createFakeBridgeHost({
1022
1265
  ...hostOptions,
1266
+ onAgentChange() {
1267
+ hostOptions.onAgentChange?.();
1268
+ changed();
1269
+ },
1023
1270
  onRequest(request) {
1024
1271
  log.requests.push(request);
1025
1272
  const params = request.params ?? {};
@@ -1110,6 +1357,7 @@ function createBrowserTestHost(options) {
1110
1357
  return {
1111
1358
  host,
1112
1359
  log,
1360
+ agent: host.agent,
1113
1361
  setNextScan(scan) {
1114
1362
  nextScan = scan;
1115
1363
  },
@@ -1142,6 +1390,11 @@ function createBrowserTestHost(options) {
1142
1390
  host.setDirectoryUsers(users);
1143
1391
  onChange?.();
1144
1392
  },
1393
+ async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
1394
+ const document2 = await host.applyRemoteUpdate(databaseId, docId, input, updateOptions);
1395
+ onChange?.();
1396
+ return document2;
1397
+ },
1145
1398
  reloadApp,
1146
1399
  dispose() {
1147
1400
  window.removeEventListener("message", onMessage);
@@ -1200,6 +1453,10 @@ var STYLE = `
1200
1453
  .htest__meta{margin-left:auto;color:#5b6478;font-size:11px;white-space:nowrap}
1201
1454
  .htest__error{color:#b42318}
1202
1455
  .htest pre{margin:4px 0 2px;padding:6px 8px;background:#fff;border:1px solid #e4e8f1;border-radius:6px;font:11px/1.4 ui-monospace,monospace;white-space:pre-wrap;word-break:break-all;max-height:260px;overflow:auto}
1456
+ .htest textarea{width:100%;min-height:70px;font:11px/1.4 ui-monospace,monospace;border:1px solid #c9d0e0;border-radius:6px;padding:6px}
1457
+ .htest__tag{display:inline-block;padding:0 6px;border-radius:999px;background:#e4e8f1;color:#334155;font-size:10px;margin-left:4px}
1458
+ .htest__tag--warn{background:#fee4e2;color:#912018}
1459
+ .htest__shot{max-width:100%;border:1px solid #e4e8f1;border-radius:6px;margin-top:4px;background:#fff}
1203
1460
  @media (max-width:720px){.htest{grid-template-columns:1fr;grid-template-rows:65vh auto}.htest__panel{border-left:0;border-top:1px solid #d7dce8}}
1204
1461
  `;
1205
1462
  function previewJson(value, maxString = 400, maxItems = 30, maxDepth = 6) {
@@ -1249,7 +1506,7 @@ function mountHavenTestHost(options) {
1249
1506
  const databases = (hostOptions.databases ?? []).filter((database) => url.overrides[database.info.id]?.mapped !== false).map((database) => {
1250
1507
  const capabilities = url.overrides[database.info.id]?.capabilities;
1251
1508
  return capabilities ? { ...database, info: { ...database.info, capabilities } } : database;
1252
- });
1509
+ }).map((database) => url.automerge === void 0 ? database : { ...database, automerge: url.automerge });
1253
1510
  const launchContext = {
1254
1511
  ...hostOptions.launchContext,
1255
1512
  ...url.locale ? { locale: url.locale } : {},
@@ -1430,14 +1687,24 @@ function mountHavenTestHost(options) {
1430
1687
  lists.scans.replaceChildren(...log.scans.map(scanEntry));
1431
1688
  lists.requests.replaceChildren(...log.requests.slice(-50).reverse().map(requestEntry));
1432
1689
  }
1690
+ let agentReady = false;
1433
1691
  const testHost = createBrowserTestHost({
1692
+ // Haven's consent dialog, as a browser confirm (scripts can switch it with agent.setConsent)
1693
+ agentConsent: ({ tool, input }) => window.confirm(`An agent wants to run "${tool}" (marked consequential).
1694
+
1695
+ ${previewJson(input, 200, 10, 3)}
1696
+
1697
+ Allow?`),
1434
1698
  ...hostOptions,
1435
1699
  databases,
1436
1700
  launchContext,
1437
1701
  enforceCapabilities: url.enforceCapabilities ?? hostOptions.enforceCapabilities,
1438
1702
  directoryUsers: url.directoryUserCount !== void 0 ? generateDirectoryUsers(url.directoryUserCount) : hostOptions.directoryUsers,
1439
1703
  frame,
1440
- onChange: render,
1704
+ onChange: () => {
1705
+ render();
1706
+ if (agentReady) renderAgent();
1707
+ },
1441
1708
  onEmbedChange(embeds) {
1442
1709
  hostOptions.onEmbedChange?.(embeds);
1443
1710
  renderEmbeds(embeds);
@@ -1574,11 +1841,213 @@ function mountHavenTestHost(options) {
1574
1841
  scanInput,
1575
1842
  hint("What attachments.scan() returns; the file is attached to the document like in Haven.")
1576
1843
  );
1844
+ const agentState = document.createElement("div");
1845
+ agentState.className = "htest__muted";
1846
+ const agentEnabled = document.createElement("label");
1847
+ agentEnabled.className = "htest__check";
1848
+ const agentEnabledBox = document.createElement("input");
1849
+ agentEnabledBox.type = "checkbox";
1850
+ agentEnabledBox.dataset.testid = "haven-test-agent-enabled";
1851
+ agentEnabledBox.checked = testHost.agent.isEnabled();
1852
+ agentEnabledBox.addEventListener("change", () => testHost.agent.setEnabled(agentEnabledBox.checked));
1853
+ agentEnabled.append(agentEnabledBox, "Agent access on (registerTools answers enabled)");
1854
+ const agentTools = document.createElement("ul");
1855
+ const toolSelect = document.createElement("select");
1856
+ toolSelect.dataset.testid = "haven-test-agent-tool";
1857
+ toolSelect.setAttribute("aria-label", "Agent tool");
1858
+ const toolInput = document.createElement("textarea");
1859
+ toolInput.dataset.testid = "haven-test-agent-input";
1860
+ toolInput.setAttribute("aria-label", "Tool input (JSON)");
1861
+ toolInput.value = "{}";
1862
+ const toolResult = document.createElement("div");
1863
+ toolResult.dataset.testid = "haven-test-agent-result";
1864
+ const callButton = button("Call tool", () => void runTool());
1865
+ callButton.dataset.testid = "haven-test-agent-call";
1866
+ const agentContext = document.createElement("pre");
1867
+ agentContext.dataset.testid = "haven-test-agent-context";
1868
+ const agentFiles = document.createElement("ul");
1869
+ const agentCalls = document.createElement("ul");
1870
+ const importInput = document.createElement("input");
1871
+ importInput.type = "file";
1872
+ importInput.multiple = true;
1873
+ importInput.dataset.testid = "haven-test-agent-import";
1874
+ importInput.setAttribute("aria-label", "Import files for the app");
1875
+ importInput.addEventListener("change", () => {
1876
+ void (async () => {
1877
+ for (const file of importInput.files ?? []) {
1878
+ await testHost.agent.importFile({ name: file.name, mimeType: file.type, data: file });
1879
+ }
1880
+ importInput.value = "";
1881
+ })();
1882
+ });
1883
+ function showOutcome(target, call) {
1884
+ const pre = document.createElement("pre");
1885
+ const outcome = call.outcome;
1886
+ if (!outcome) {
1887
+ pre.textContent = "running\u2026";
1888
+ target.replaceChildren(pre);
1889
+ return;
1890
+ }
1891
+ if (outcome.ok) {
1892
+ pre.textContent = previewJson(outcome.result, 2e3, 60, 8);
1893
+ } else {
1894
+ pre.className = "htest__error";
1895
+ pre.textContent = `${outcome.error.code}: ${outcome.error.message}${outcome.error.requiredAction ? `
1896
+ \u2192 ${outcome.error.requiredAction}` : ""}`;
1897
+ }
1898
+ const images = [];
1899
+ const collect = (value, depth) => {
1900
+ if (depth > 4 || images.length >= 4) return;
1901
+ if (typeof value === "string" && /^data:image\/(png|jpeg|gif|webp|svg\+xml);base64,/.test(value)) images.push(value);
1902
+ else if (value && typeof value === "object") for (const child of Object.values(value)) collect(child, depth + 1);
1903
+ };
1904
+ if (outcome.ok) collect(outcome.result, 0);
1905
+ target.replaceChildren(
1906
+ pre,
1907
+ ...images.map((src) => {
1908
+ const img = document.createElement("img");
1909
+ img.className = "htest__shot";
1910
+ img.src = src;
1911
+ img.alt = "image in the tool result";
1912
+ return img;
1913
+ })
1914
+ );
1915
+ }
1916
+ async function runTool() {
1917
+ let input;
1918
+ try {
1919
+ const parsed = JSON.parse(toolInput.value || "{}");
1920
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("The input must be a JSON object.");
1921
+ input = parsed;
1922
+ } catch (error) {
1923
+ const pre = document.createElement("pre");
1924
+ pre.className = "htest__error";
1925
+ pre.textContent = error instanceof Error ? error.message : String(error);
1926
+ toolResult.replaceChildren(pre);
1927
+ return;
1928
+ }
1929
+ callButton.disabled = true;
1930
+ try {
1931
+ await testHost.agent.call(toolSelect.value, input);
1932
+ const last = testHost.agent.calls().at(-1);
1933
+ if (last) showOutcome(toolResult, last);
1934
+ } finally {
1935
+ callButton.disabled = false;
1936
+ }
1937
+ }
1938
+ function download(name, mimeType, data) {
1939
+ const link = document.createElement("a");
1940
+ link.textContent = name;
1941
+ link.download = name;
1942
+ link.href = URL.createObjectURL(new Blob([data.slice().buffer], { type: mimeType }));
1943
+ return link;
1944
+ }
1945
+ const openCalls = /* @__PURE__ */ new Set();
1946
+ function renderAgent() {
1947
+ const agent = testHost.agent;
1948
+ const tools = agent.tools();
1949
+ agentState.textContent = `prefix "${agent.prefix}" \xB7 ${tools.length} tool(s) \xB7 ${agent.isEnabled() ? "access on" : "access off"}`;
1950
+ agentEnabledBox.checked = agent.isEnabled();
1951
+ agentTools.replaceChildren(
1952
+ ...tools.map((tool) => {
1953
+ const li = document.createElement("li");
1954
+ const details = document.createElement("details");
1955
+ const summary = document.createElement("summary");
1956
+ const name = document.createElement("span");
1957
+ name.textContent = `${agent.prefix}_${tool.name}`;
1958
+ summary.append(name);
1959
+ const hints = tool.annotations ?? {};
1960
+ if (hints.readOnlyHint) summary.append(Object.assign(document.createElement("span"), { className: "htest__tag", textContent: "read-only" }));
1961
+ if (hints.consequentialHint) summary.append(Object.assign(document.createElement("span"), { className: "htest__tag htest__tag--warn", textContent: "asks first" }));
1962
+ const description = document.createElement("p");
1963
+ description.textContent = tool.description;
1964
+ const schema = document.createElement("pre");
1965
+ schema.textContent = previewJson(tool.inputSchema, 400, 60, 10);
1966
+ details.append(summary, description, schema);
1967
+ li.append(details);
1968
+ return li;
1969
+ })
1970
+ );
1971
+ const selected = toolSelect.value;
1972
+ toolSelect.replaceChildren(...tools.map((tool) => new Option(`${agent.prefix}_${tool.name}`, tool.name)));
1973
+ if (tools.some((tool) => tool.name === selected)) toolSelect.value = selected;
1974
+ callButton.disabled = tools.length === 0;
1975
+ agentContext.textContent = previewJson(agent.context(), 400, 30, 6);
1976
+ agentFiles.replaceChildren(
1977
+ ...agent.files().map((file) => {
1978
+ const li = document.createElement("li");
1979
+ const ref = document.createElement("code");
1980
+ ref.textContent = file.fileRef;
1981
+ const copy = button("copy ref", () => void navigator.clipboard?.writeText(file.fileRef));
1982
+ li.append(
1983
+ file.source === "app" ? "from app: " : "for app: ",
1984
+ file.source === "app" ? download(file.name, file.mimeType, file.data) : file.name,
1985
+ ` (${file.data.length} bytes) `,
1986
+ ref,
1987
+ " ",
1988
+ copy
1989
+ );
1990
+ return li;
1991
+ })
1992
+ );
1993
+ agentCalls.replaceChildren(
1994
+ ...agent.calls().slice(-20).reverse().map((call) => {
1995
+ const li = document.createElement("li");
1996
+ const details = document.createElement("details");
1997
+ const summary = document.createElement("summary");
1998
+ const label = document.createElement("span");
1999
+ label.textContent = call.tool;
2000
+ if (call.outcome && !call.outcome.ok) label.className = "htest__error";
2001
+ const meta = document.createElement("span");
2002
+ meta.className = "htest__meta";
2003
+ meta.textContent = call.outcome ? `${call.outcome.durationMs} ms` : "\u2026";
2004
+ summary.append(label, meta);
2005
+ const body = document.createElement("div");
2006
+ const input = document.createElement("pre");
2007
+ input.textContent = previewJson(call.input);
2008
+ details.append(summary, body);
2009
+ details.addEventListener("toggle", () => {
2010
+ if (details.open) {
2011
+ openCalls.add(call.callId);
2012
+ const out = document.createElement("div");
2013
+ showOutcome(out, call);
2014
+ body.replaceChildren(input, out);
2015
+ } else openCalls.delete(call.callId);
2016
+ });
2017
+ if (openCalls.has(call.callId)) details.open = true;
2018
+ li.append(details);
2019
+ return li;
2020
+ })
2021
+ );
2022
+ }
2023
+ const callRow = document.createElement("div");
2024
+ callRow.className = "htest__row";
2025
+ callRow.append(toolSelect, callButton);
2026
+ section(
2027
+ "Agent tools",
2028
+ agentState,
2029
+ agentEnabled,
2030
+ agentTools,
2031
+ callRow,
2032
+ toolInput,
2033
+ toolResult,
2034
+ hint("Calls a tool as an agent would; tools marked \u201Casks first\u201D go through a confirm, like Haven's consent dialog.")
2035
+ );
2036
+ section(
2037
+ "Agent files",
2038
+ importInput,
2039
+ hint("Import puts a file in the exchange for the app (takeFile); pass its fileRef to a tool. Files the app hands over (provideFile) can be downloaded."),
2040
+ agentFiles
2041
+ );
2042
+ section("Agent context", agentContext);
2043
+ section("Agent calls (latest first)", agentCalls);
1577
2044
  section("Notifications", lists.notifications);
1578
2045
  section("Previews", lists.previews);
1579
2046
  section("Scans", lists.scans);
1580
2047
  section("Requests (latest first, click for details)", lists.requests);
1581
2048
  render();
2049
+ agentReady = true;
2050
+ renderAgent();
1582
2051
  syncUrl();
1583
2052
  window.__havenTestHost = testHost;
1584
2053
  return testHost;
@@ -1882,23 +2351,23 @@ function mergeLaunchContext(current, patch) {
1882
2351
  ...database,
1883
2352
  capabilities: [...database.capabilities]
1884
2353
  })),
1885
- views: current.views.map((view) => ({
1886
- ...view,
1887
- sources: view.sources.map((source) => ({ ...source })),
1888
- columns: view.columns.map((column) => ({
2354
+ views: current.views.map((view2) => ({
2355
+ ...view2,
2356
+ sources: view2.sources.map((source) => ({ ...source })),
2357
+ columns: view2.columns.map((column) => ({
1889
2358
  ...column,
1890
2359
  expression: column.expression.mode === "field" ? { ...column.expression } : {
1891
2360
  mode: "formula",
1892
2361
  expression: structuredClone(column.expression.expression)
1893
2362
  }
1894
2363
  })),
1895
- filter: view.filter.mode === "rules" ? {
2364
+ filter: view2.filter.mode === "rules" ? {
1896
2365
  mode: "rules",
1897
- match: view.filter.match,
1898
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2366
+ match: view2.filter.match,
2367
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
1899
2368
  } : {
1900
2369
  mode: "formula",
1901
- expression: structuredClone(view.filter.expression)
2370
+ expression: structuredClone(view2.filter.expression)
1902
2371
  }
1903
2372
  }))
1904
2373
  };
@@ -1920,41 +2389,41 @@ function mergeLaunchContext(current, patch) {
1920
2389
  ...database,
1921
2390
  capabilities: [...database.capabilities]
1922
2391
  })),
1923
- views: patch.views ? patch.views.map((view) => ({
1924
- ...view,
1925
- sources: view.sources.map((source) => ({ ...source })),
1926
- columns: view.columns.map((column) => ({
2392
+ views: patch.views ? patch.views.map((view2) => ({
2393
+ ...view2,
2394
+ sources: view2.sources.map((source) => ({ ...source })),
2395
+ columns: view2.columns.map((column) => ({
1927
2396
  ...column,
1928
2397
  expression: column.expression.mode === "field" ? { ...column.expression } : {
1929
2398
  mode: "formula",
1930
2399
  expression: structuredClone(column.expression.expression)
1931
2400
  }
1932
2401
  })),
1933
- filter: view.filter.mode === "rules" ? {
2402
+ filter: view2.filter.mode === "rules" ? {
1934
2403
  mode: "rules",
1935
- match: view.filter.match,
1936
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2404
+ match: view2.filter.match,
2405
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
1937
2406
  } : {
1938
2407
  mode: "formula",
1939
- expression: structuredClone(view.filter.expression)
2408
+ expression: structuredClone(view2.filter.expression)
1940
2409
  }
1941
- })) : current.views.map((view) => ({
1942
- ...view,
1943
- sources: view.sources.map((source) => ({ ...source })),
1944
- columns: view.columns.map((column) => ({
2410
+ })) : current.views.map((view2) => ({
2411
+ ...view2,
2412
+ sources: view2.sources.map((source) => ({ ...source })),
2413
+ columns: view2.columns.map((column) => ({
1945
2414
  ...column,
1946
2415
  expression: column.expression.mode === "field" ? { ...column.expression } : {
1947
2416
  mode: "formula",
1948
2417
  expression: structuredClone(column.expression.expression)
1949
2418
  }
1950
2419
  })),
1951
- filter: view.filter.mode === "rules" ? {
2420
+ filter: view2.filter.mode === "rules" ? {
1952
2421
  mode: "rules",
1953
- match: view.filter.match,
1954
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2422
+ match: view2.filter.match,
2423
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
1955
2424
  } : {
1956
2425
  mode: "formula",
1957
- expression: structuredClone(view.filter.expression)
2426
+ expression: structuredClone(view2.filter.expression)
1958
2427
  }
1959
2428
  }))
1960
2429
  };
@@ -2045,9 +2514,12 @@ function mockDecryptionKeyId(document2) {
2045
2514
  }
2046
2515
  return { decryptionKeyId: document2.decryptionKeyId };
2047
2516
  }
2048
- function loadMockAutomergeDocument(document2) {
2517
+ function loadMockAutomergeDocument(document2, actor) {
2049
2518
  if (document2.automergeBinary) {
2050
- return Automerge.load(document2.automergeBinary);
2519
+ return Automerge.load(
2520
+ document2.automergeBinary,
2521
+ actor ? { actor } : void 0
2522
+ );
2051
2523
  }
2052
2524
  const initialDoc = Automerge.from({
2053
2525
  ...structuredClone(document2.data),
@@ -2058,9 +2530,401 @@ function loadMockAutomergeDocument(document2) {
2058
2530
  }
2059
2531
  function persistMockAutomergeDocument(document2, automergeDoc) {
2060
2532
  document2.automergeBinary = Automerge.save(automergeDoc);
2061
- document2.data = structuredClone(automergeDoc);
2533
+ document2.data = projectMockAutomergeValue(
2534
+ Automerge.load(document2.automergeBinary)
2535
+ );
2062
2536
  document2.heads = Automerge.getHeads(automergeDoc);
2063
2537
  }
2538
+ var MOCK_AUTOMERGE_SEED_ACTOR = "5eed0000000000000000000000000000";
2539
+ var MOCK_AUTOMERGE_LOCAL_ACTOR = "10ca1000000000000000000000000000";
2540
+ var MOCK_AUTOMERGE_REMOTE_ACTOR = "2e307e00000000000000000000000000";
2541
+ function projectMockAutomergeValue(value, depth = 0) {
2542
+ if (value === null || typeof value !== "object") {
2543
+ return value;
2544
+ }
2545
+ if (Automerge.isCounter(value)) {
2546
+ return value.value;
2547
+ }
2548
+ if (Automerge.isImmutableString(value)) {
2549
+ return String(value);
2550
+ }
2551
+ if (value instanceof Date) {
2552
+ return value.toISOString();
2553
+ }
2554
+ if (value instanceof Uint8Array) {
2555
+ return value.slice();
2556
+ }
2557
+ if (depth > 100) {
2558
+ throw new Error("Document value is nested too deeply");
2559
+ }
2560
+ if (Array.isArray(value)) {
2561
+ return value.map((entry) => projectMockAutomergeValue(entry, depth + 1));
2562
+ }
2563
+ return Object.fromEntries(
2564
+ Object.entries(value).map(([key, entry]) => [
2565
+ key,
2566
+ projectMockAutomergeValue(entry, depth + 1)
2567
+ ])
2568
+ );
2569
+ }
2570
+ function hydrateMockAutomergeValues(value, depth = 0) {
2571
+ if (value === null || typeof value !== "object") {
2572
+ return value;
2573
+ }
2574
+ if (depth > 100) {
2575
+ throw new Error("Document value is nested too deeply");
2576
+ }
2577
+ if (value instanceof Uint8Array || value instanceof Date || Automerge.isCounter(value) || Automerge.isImmutableString(value)) {
2578
+ return value;
2579
+ }
2580
+ if (Array.isArray(value)) {
2581
+ return value.map(
2582
+ (entry) => entry === void 0 ? null : hydrateMockAutomergeValues(entry, depth + 1)
2583
+ );
2584
+ }
2585
+ const record = value;
2586
+ if (Object.prototype.hasOwnProperty.call(record, MINDOODB_APP_VALUE_TAG)) {
2587
+ switch (record[MINDOODB_APP_VALUE_TAG]) {
2588
+ case "atomic":
2589
+ if (typeof record.value !== "string") {
2590
+ throw new Error('Typed value "atomic" needs a string value');
2591
+ }
2592
+ return new Automerge.ImmutableString(record.value);
2593
+ case "counter":
2594
+ assertMockCounterAmount(record.value, 'Typed value "counter"');
2595
+ return new Automerge.Counter(record.value);
2596
+ case "timestamp":
2597
+ return new Date(toMockTimestamp(record.value));
2598
+ default:
2599
+ throw new Error(
2600
+ `Unknown typed value ${JSON.stringify(record[MINDOODB_APP_VALUE_TAG])}; the "${MINDOODB_APP_VALUE_TAG}" key is reserved`
2601
+ );
2602
+ }
2603
+ }
2604
+ const result = {};
2605
+ for (const [key, entry] of Object.entries(record)) {
2606
+ if (entry !== void 0) {
2607
+ result[key] = hydrateMockAutomergeValues(entry, depth + 1);
2608
+ }
2609
+ }
2610
+ return result;
2611
+ }
2612
+ function createMockAutomergeDocument(values, actor, time) {
2613
+ const entries = Object.entries(values).filter(([, value]) => value !== void 0);
2614
+ const initial = Automerge.init({ actor });
2615
+ const options = time === void 0 ? {} : { time };
2616
+ if (entries.length === 0) {
2617
+ return Automerge.emptyChange(initial, options);
2618
+ }
2619
+ return Automerge.change(initial, options, (doc) => {
2620
+ for (const [key, value] of entries) {
2621
+ doc[key] = hydrateMockAutomergeValues(value);
2622
+ }
2623
+ });
2624
+ }
2625
+ function validateMockJsonPath(path, label) {
2626
+ if (!Array.isArray(path) || path.length === 0) {
2627
+ throw new Error(`${label} path must contain at least one segment`);
2628
+ }
2629
+ for (const segment of path) {
2630
+ if (typeof segment !== "string" && typeof segment !== "number") {
2631
+ throw new Error(`${label} path segments must be strings or numbers`);
2632
+ }
2633
+ }
2634
+ }
2635
+ function assertMockNonNegativeInteger(value, label) {
2636
+ if (!Number.isInteger(value) || value < 0) {
2637
+ throw new Error(`${label} must be a non-negative integer`);
2638
+ }
2639
+ }
2640
+ function validateMockJsonPatch(patch) {
2641
+ const operationCount = (patch.set?.length ?? 0) + (patch.unset?.length ?? 0) + (patch.listDelete?.length ?? 0) + (patch.listInsert?.length ?? 0) + (patch.textSplice?.length ?? 0) + (patch.textMark?.length ?? 0) + (patch.textUnmark?.length ?? 0) + (patch.counterIncrement?.length ?? 0);
2642
+ if (operationCount === 0) {
2643
+ throw new Error("JSON patch must include at least one operation");
2644
+ }
2645
+ for (const operation of patch.set ?? []) {
2646
+ validateMockJsonPath(operation.path, "JSON set");
2647
+ hydrateMockAutomergeValues(operation.value);
2648
+ }
2649
+ for (const operation of patch.unset ?? []) {
2650
+ validateMockJsonPath(operation.path, "JSON unset");
2651
+ }
2652
+ for (const operation of patch.listDelete ?? []) {
2653
+ validateMockJsonPath(operation.path, "JSON listDelete");
2654
+ assertMockNonNegativeInteger(operation.index, "JSON listDelete index");
2655
+ assertMockNonNegativeInteger(operation.deleteCount, "JSON listDelete deleteCount");
2656
+ }
2657
+ for (const operation of patch.listInsert ?? []) {
2658
+ validateMockJsonPath(operation.path, "JSON listInsert");
2659
+ assertMockNonNegativeInteger(operation.index, "JSON listInsert index");
2660
+ if (!Array.isArray(operation.values)) {
2661
+ throw new Error("JSON listInsert values must be an array");
2662
+ }
2663
+ hydrateMockAutomergeValues(operation.values);
2664
+ }
2665
+ for (const operation of patch.textSplice ?? []) {
2666
+ validateMockJsonPath(operation.path, "JSON textSplice");
2667
+ assertMockNonNegativeInteger(operation.index, "JSON textSplice index");
2668
+ assertMockNonNegativeInteger(operation.deleteCount, "JSON textSplice deleteCount");
2669
+ if (operation.insert !== void 0 && typeof operation.insert !== "string") {
2670
+ throw new Error("JSON textSplice insert must be a string");
2671
+ }
2672
+ }
2673
+ for (const operation of [...patch.textMark ?? [], ...patch.textUnmark ?? []]) {
2674
+ validateMockJsonPath(operation.path, "JSON text mark");
2675
+ assertMockNonNegativeInteger(operation.index, "JSON text mark index");
2676
+ if (!Number.isInteger(operation.length) || operation.length <= 0) {
2677
+ throw new Error("JSON text mark length must be a positive integer");
2678
+ }
2679
+ }
2680
+ for (const operation of patch.counterIncrement ?? []) {
2681
+ validateMockJsonPath(operation.path, "JSON counterIncrement");
2682
+ assertMockCounterAmount(operation.delta, "JSON counterIncrement delta");
2683
+ }
2684
+ }
2685
+ function ensureMockAutomergeParent(target, path) {
2686
+ let parent = target;
2687
+ for (let index = 0; index < path.length - 1; index += 1) {
2688
+ const segment = path[index];
2689
+ const nextSegment = path[index + 1];
2690
+ if (parent[segment] === void 0 || parent[segment] === null) {
2691
+ parent[segment] = typeof nextSegment === "number" ? [] : {};
2692
+ }
2693
+ parent = parent[segment];
2694
+ if (parent === null || typeof parent !== "object") {
2695
+ throw new Error(
2696
+ `Cannot apply JSON patch through non-object path segment '${String(segment)}'`
2697
+ );
2698
+ }
2699
+ }
2700
+ return parent;
2701
+ }
2702
+ function readMockAutomergeValue(target, path) {
2703
+ let value = target;
2704
+ for (const segment of path) {
2705
+ value = value?.[segment];
2706
+ }
2707
+ return value;
2708
+ }
2709
+ function setMockAutomergeValue(target, path, value) {
2710
+ const parent = ensureMockAutomergeParent(target, path);
2711
+ parent[path[path.length - 1]] = value;
2712
+ }
2713
+ function unsetMockAutomergeValue(target, path) {
2714
+ let parent = target;
2715
+ for (let index = 0; index < path.length - 1; index += 1) {
2716
+ parent = parent?.[path[index]];
2717
+ if (parent === null || typeof parent !== "object") {
2718
+ return;
2719
+ }
2720
+ }
2721
+ delete parent[path[path.length - 1]];
2722
+ }
2723
+ function readMockAutomergeList(target, path) {
2724
+ const value = readMockAutomergeValue(target, path);
2725
+ if (!Array.isArray(value)) {
2726
+ throw new Error(
2727
+ `Cannot apply JSON list operation to non-array value at '${path.map(String).join(".")}'`
2728
+ );
2729
+ }
2730
+ return value;
2731
+ }
2732
+ function ensureMockAutomergeList(target, path) {
2733
+ const parent = ensureMockAutomergeParent(target, path);
2734
+ const leaf = path[path.length - 1];
2735
+ if (parent[leaf] === void 0 || parent[leaf] === null) {
2736
+ parent[leaf] = [];
2737
+ }
2738
+ return readMockAutomergeList(target, path);
2739
+ }
2740
+ function clampMockIndex(index, length) {
2741
+ if (!Number.isFinite(index)) {
2742
+ return length;
2743
+ }
2744
+ return Math.max(0, Math.min(Math.trunc(index), length));
2745
+ }
2746
+ function ensureMockAutomergeText(target, path, label) {
2747
+ const value = readMockAutomergeValue(target, path);
2748
+ if (value === void 0 || value === null) {
2749
+ setMockAutomergeValue(target, path, "");
2750
+ } else if (typeof value !== "string") {
2751
+ throw new Error(
2752
+ `Cannot apply ${label} to non-string value at '${path.map(String).join(".")}'`
2753
+ );
2754
+ }
2755
+ }
2756
+ function spliceMockAutomergeText(target, path, index, deleteCount, insert) {
2757
+ const text = readMockAutomergeValue(target, path);
2758
+ const length = typeof text === "string" ? text.length : 0;
2759
+ const start = clampMockIndex(index, length);
2760
+ Automerge.splice(
2761
+ target,
2762
+ path,
2763
+ start,
2764
+ Math.max(0, Math.min(deleteCount, length - start)),
2765
+ insert
2766
+ );
2767
+ }
2768
+ function reviveMockRichTextValue(value) {
2769
+ if (Array.isArray(value)) {
2770
+ return value.map((entry) => reviveMockRichTextValue(entry));
2771
+ }
2772
+ if (!value || typeof value !== "object") {
2773
+ return value;
2774
+ }
2775
+ const record = value;
2776
+ if (record.type === "immutableString" && typeof record.value === "string") {
2777
+ return new Automerge.ImmutableString(record.value);
2778
+ }
2779
+ return Object.fromEntries(
2780
+ Object.entries(record).map(([key, entry]) => [key, reviveMockRichTextValue(entry)])
2781
+ );
2782
+ }
2783
+ function applyMockAutomergeJsonOperations(target, patch) {
2784
+ for (const operation of patch.set ?? []) {
2785
+ setMockAutomergeValue(target, operation.path, hydrateMockAutomergeValues(operation.value));
2786
+ }
2787
+ for (const operation of patch.unset ?? []) {
2788
+ unsetMockAutomergeValue(target, operation.path);
2789
+ }
2790
+ for (const operation of patch.listDelete ?? []) {
2791
+ readMockAutomergeList(target, operation.path).splice(operation.index, operation.deleteCount);
2792
+ }
2793
+ for (const operation of patch.listInsert ?? []) {
2794
+ ensureMockAutomergeList(target, operation.path).splice(
2795
+ operation.index,
2796
+ 0,
2797
+ ...hydrateMockAutomergeValues(operation.values)
2798
+ );
2799
+ }
2800
+ for (const operation of patch.textSplice ?? []) {
2801
+ ensureMockAutomergeText(target, operation.path, "JSON textSplice");
2802
+ spliceMockAutomergeText(
2803
+ target,
2804
+ operation.path,
2805
+ operation.index,
2806
+ operation.deleteCount,
2807
+ operation.insert ?? ""
2808
+ );
2809
+ }
2810
+ for (const operation of patch.textMark ?? []) {
2811
+ for (const [name, value] of Object.entries(operation.marks)) {
2812
+ Automerge.mark(
2813
+ target,
2814
+ operation.path,
2815
+ { start: operation.index, end: operation.index + operation.length, expand: "none" },
2816
+ name,
2817
+ reviveMockRichTextValue(value)
2818
+ );
2819
+ }
2820
+ }
2821
+ for (const operation of patch.textUnmark ?? []) {
2822
+ for (const name of operation.names) {
2823
+ Automerge.unmark(
2824
+ target,
2825
+ operation.path,
2826
+ { start: operation.index, end: operation.index + operation.length, expand: "none" },
2827
+ name
2828
+ );
2829
+ }
2830
+ }
2831
+ for (const operation of patch.counterIncrement ?? []) {
2832
+ const parent = ensureMockAutomergeParent(target, operation.path);
2833
+ const leaf = operation.path[operation.path.length - 1];
2834
+ const existing = parent[leaf];
2835
+ if (existing === void 0 || existing === null) {
2836
+ parent[leaf] = new Automerge.Counter(operation.delta);
2837
+ } else if (Automerge.isCounter(existing)) {
2838
+ existing.increment(operation.delta);
2839
+ } else {
2840
+ throw new Error(
2841
+ `Cannot increment non-counter value at '${operation.path.map(String).join(".")}'; create it with MindooValue.counter() first`
2842
+ );
2843
+ }
2844
+ }
2845
+ }
2846
+ function changeMockAutomergeDocument(doc, baseHeads, apply) {
2847
+ if (baseHeads && baseHeads.length > 0) {
2848
+ const { newDoc } = Automerge.changeAt(doc, baseHeads, apply);
2849
+ return Automerge.load(Automerge.save(newDoc), { actor: Automerge.getActorId(doc) });
2850
+ }
2851
+ return Automerge.change(doc, apply);
2852
+ }
2853
+ function applyMockAutomergeUpdate(doc, patch) {
2854
+ let next = doc;
2855
+ if (patch.set || (patch.unset?.length ?? 0) > 0) {
2856
+ next = Automerge.change(next, (target) => {
2857
+ for (const [key, value] of Object.entries(patch.set ?? {})) {
2858
+ if (value !== void 0) {
2859
+ target[key] = hydrateMockAutomergeValues(value);
2860
+ }
2861
+ }
2862
+ for (const key of patch.unset ?? []) {
2863
+ delete target[key];
2864
+ }
2865
+ });
2866
+ }
2867
+ if (patch.json) {
2868
+ const json = patch.json;
2869
+ validateMockJsonPatch(json);
2870
+ next = changeMockAutomergeDocument(
2871
+ next,
2872
+ json.baseHeads,
2873
+ (target) => applyMockAutomergeJsonOperations(target, json)
2874
+ );
2875
+ }
2876
+ for (const textPatch of patch.text ?? []) {
2877
+ validateMockJsonPath(textPatch.path, "Text patch");
2878
+ next = changeMockAutomergeDocument(next, textPatch.baseHeads, (target) => {
2879
+ ensureMockAutomergeText(target, textPatch.path, "text patch");
2880
+ for (const edit of textPatch.edits) {
2881
+ spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
2882
+ }
2883
+ });
2884
+ }
2885
+ for (const richTextPatch of patch.richText ?? []) {
2886
+ validateMockJsonPath(richTextPatch.path, "Rich-text patch");
2887
+ const spans = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
2888
+ next = Automerge.change(next, (target) => {
2889
+ setMockAutomergeValue(target, richTextPatch.path, hydrateMockAutomergeValues(structuredClone(spans)));
2890
+ });
2891
+ }
2892
+ for (const stepPatch of patch.richTextSteps ?? []) {
2893
+ validateMockJsonPath(stepPatch.path, "Rich-text steps patch");
2894
+ next = changeMockAutomergeDocument(next, stepPatch.baseHeads, (target) => {
2895
+ const current = readMockAutomergeValue(target, stepPatch.path);
2896
+ if (current !== void 0 && current !== null && typeof current !== "string") {
2897
+ setMockAutomergeValue(
2898
+ target,
2899
+ stepPatch.path,
2900
+ materializeMockRichTextValue(projectMockAutomergeValue(current))
2901
+ );
2902
+ }
2903
+ ensureMockAutomergeText(target, stepPatch.path, "rich-text steps");
2904
+ for (const step of stepPatch.steps) {
2905
+ spliceMockAutomergeText(target, stepPatch.path, step.index, step.deleteCount, step.insert ?? "");
2906
+ }
2907
+ });
2908
+ }
2909
+ return next;
2910
+ }
2911
+ function forkMockAutomergeDocument(doc, baseHeads, actor) {
2912
+ if (!baseHeads || baseHeads.length === 0) {
2913
+ return Automerge.clone(doc, { actor });
2914
+ }
2915
+ const heads = new Set(baseHeads);
2916
+ let ownLatest;
2917
+ for (const change2 of Automerge.getAllChanges(doc)) {
2918
+ const decoded = Automerge.decodeChange(change2);
2919
+ if (decoded.actor === actor) {
2920
+ ownLatest = decoded.hash;
2921
+ }
2922
+ }
2923
+ if (ownLatest) {
2924
+ heads.add(ownLatest);
2925
+ }
2926
+ return Automerge.clone(Automerge.view(doc, [...heads]), { actor });
2927
+ }
2064
2928
  function getFieldValue2(source, field) {
2065
2929
  return field.split(".").reduce((current, part) => {
2066
2930
  if (current && typeof current === "object" && part in current) {
@@ -2373,9 +3237,9 @@ function createLiveMockViewNavigator(initial, rebuild, subscribe) {
2373
3237
  listeners.clear();
2374
3238
  if (pendingNotify) clearTimeout(pendingNotify);
2375
3239
  }
2376
- const navigator = await current();
2377
- const member = navigator[property];
2378
- return typeof member === "function" ? member.apply(navigator, args) : member;
3240
+ const navigator2 = await current();
3241
+ const member = navigator2[property];
3242
+ return typeof member === "function" ? member.apply(navigator2, args) : member;
2379
3243
  };
2380
3244
  }
2381
3245
  });
@@ -2913,9 +3777,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
2913
3777
  let createCounter = 0;
2914
3778
  let changeCounter = 0;
2915
3779
  const defaultViewFactory = async () => createDefaultViewNavigator();
2916
- const storedDocuments = /* @__PURE__ */ new Map();
3780
+ const automergeMode = definition.automerge === true;
3781
+ let changeSequence = 0;
3782
+ const changedAt = /* @__PURE__ */ new Map();
3783
+ const storedDocuments = new class extends Map {
3784
+ set(id, document2) {
3785
+ changedAt.set(id, ++changeSequence);
3786
+ return super.set(id, document2);
3787
+ }
3788
+ }();
2917
3789
  for (const seed of definition.documents ?? []) {
2918
- storedDocuments.set(seed.id, {
3790
+ const stored = {
2919
3791
  id: seed.id,
2920
3792
  data: structuredClone(seed.data),
2921
3793
  heads: seed.heads ? [...seed.heads] : void 0,
@@ -2926,7 +3798,14 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
2926
3798
  decryptionKeyId: seed.decryptionKeyId,
2927
3799
  authorLabel: seed.authorLabel,
2928
3800
  createdAt: seed.createdAt
2929
- });
3801
+ };
3802
+ if (automergeMode) {
3803
+ persistMockAutomergeDocument(
3804
+ stored,
3805
+ createMockAutomergeDocument(seed.data, MOCK_AUTOMERGE_SEED_ACTOR, 0)
3806
+ );
3807
+ }
3808
+ storedDocuments.set(seed.id, stored);
2930
3809
  }
2931
3810
  const liveQuerySubscriptions = /* @__PURE__ */ new Set();
2932
3811
  const refreshLiveQueries = () => {
@@ -2969,6 +3848,15 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
2969
3848
  throw new Error("Recipient mutators require a document created with `recipients`");
2970
3849
  }
2971
3850
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3851
+ if (automergeMode) {
3852
+ return writeAutomergeDocument(
3853
+ docId,
3854
+ existing,
3855
+ Automerge.change(loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR), (target) => {
3856
+ target._encryptFor = hydrateMockAutomergeValues(nextEncryptFor);
3857
+ })
3858
+ );
3859
+ }
2972
3860
  const updated = {
2973
3861
  id: docId,
2974
3862
  data: { ...existing.data, _encryptFor: nextEncryptFor },
@@ -2987,6 +3875,28 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
2987
3875
  notifyLiveQueries();
2988
3876
  return updated;
2989
3877
  }
3878
+ function writeAutomergeDocument(docId, existing, automergeDoc) {
3879
+ const stored = {
3880
+ ...existing ?? {},
3881
+ id: docId,
3882
+ data: {},
3883
+ attachments: existing?.attachments ? structuredClone(existing.attachments) : [],
3884
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3885
+ isDeleted: false,
3886
+ decryptionKeyId: existing?.decryptionKeyId ?? "default"
3887
+ };
3888
+ persistMockAutomergeDocument(stored, automergeDoc);
3889
+ storedDocuments.set(docId, stored);
3890
+ notifyLiveQueries();
3891
+ return {
3892
+ id: docId,
3893
+ data: structuredClone(stored.data),
3894
+ heads: [...stored.heads ?? []],
3895
+ attachments: structuredClone(stored.attachments ?? []),
3896
+ updatedAt: stored.updatedAt,
3897
+ ...mockDecryptionKeyId(stored)
3898
+ };
3899
+ }
2990
3900
  const defaultDocuments = {
2991
3901
  async query(query) {
2992
3902
  return runMockDocumentQuery(
@@ -3039,12 +3949,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3039
3949
  const offset = decodeMockListCursor(query?.cursor) + skip;
3040
3950
  const metadataOnly = query?.metadataOnly ?? false;
3041
3951
  const idPrefix = query?.idPrefix;
3042
- const items = Array.from(storedDocuments.values()).filter((document2) => !document2.inaccessible).filter(
3952
+ const after = automergeMode ? decodeMockListCursor(query?.cursor) : 0;
3953
+ const items = Array.from(storedDocuments.values()).filter((document2) => !automergeMode || (changedAt.get(document2.id) ?? 0) > after).filter((document2) => !document2.inaccessible).filter(
3043
3954
  (document2) => status === "all" ? true : status === "deleted" ? document2.isDeleted : !document2.isDeleted
3044
3955
  ).filter((document2) => matchesMockIdPrefix(document2.id, idPrefix)).filter(
3045
3956
  (document2) => metadataOnly ? true : matchesDocumentFilter(document2, query?.filter)
3046
- ).sort((left, right) => left.id.localeCompare(right.id));
3047
- const page = items.slice(offset, offset + limit).map((document2) => {
3957
+ ).sort(
3958
+ (left, right) => automergeMode ? (changedAt.get(left.id) ?? 0) - (changedAt.get(right.id) ?? 0) : left.id.localeCompare(right.id)
3959
+ );
3960
+ const pageStart = automergeMode ? skip : offset;
3961
+ const pageDocuments = items.slice(pageStart, pageStart + limit);
3962
+ const page = pageDocuments.map((document2) => {
3048
3963
  if (metadataOnly) {
3049
3964
  return {
3050
3965
  id: document2.id,
@@ -3065,6 +3980,13 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3065
3980
  isDeleted: status !== "existing" ? document2.isDeleted : void 0
3066
3981
  };
3067
3982
  });
3983
+ if (automergeMode) {
3984
+ const last = pageDocuments[pageDocuments.length - 1];
3985
+ return {
3986
+ items: page,
3987
+ nextCursor: last ? String(changedAt.get(last.id) ?? after) : null
3988
+ };
3989
+ }
3068
3990
  const nextCursor = offset + page.length < items.length ? String(offset + page.length) : null;
3069
3991
  return {
3070
3992
  items: page,
@@ -3073,7 +3995,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3073
3995
  },
3074
3996
  async getHeadCursor() {
3075
3997
  return {
3076
- cursor: String(storedDocuments.size)
3998
+ cursor: String(automergeMode ? changeSequence : storedDocuments.size)
3077
3999
  };
3078
4000
  },
3079
4001
  async listInaccessible(query) {
@@ -3169,7 +4091,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3169
4091
  const currentDoc = loadMockAutomergeDocument(document2);
3170
4092
  const [mergedDoc] = Automerge.applyChanges(
3171
4093
  currentDoc,
3172
- patch.changes.map((change) => new Uint8Array(change))
4094
+ patch.changes.map((change2) => new Uint8Array(change2))
3173
4095
  );
3174
4096
  persistMockAutomergeDocument(document2, mergedDoc);
3175
4097
  document2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -3182,7 +4104,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3182
4104
  );
3183
4105
  changesSince = {
3184
4106
  sinceHeads: [...patch.replicaHeads],
3185
- changes: missingChanges.map((change) => cloneBytes(new Uint8Array(change)))
4107
+ changes: missingChanges.map((change2) => cloneBytes(new Uint8Array(change2)))
3186
4108
  };
3187
4109
  }
3188
4110
  return {
@@ -3219,7 +4141,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3219
4141
  if (existing.isDeleted) {
3220
4142
  existing.isDeleted = false;
3221
4143
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3222
- existing.heads = [`mock-head-${++changeCounter}`];
4144
+ if (!automergeMode) {
4145
+ existing.heads = [`mock-head-${++changeCounter}`];
4146
+ }
4147
+ storedDocuments.set(callerId, existing);
3223
4148
  notifyLiveQueries();
3224
4149
  }
3225
4150
  return {
@@ -3248,11 +4173,39 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3248
4173
  "recipients: [] with includeSelf: false would produce a document nobody can read"
3249
4174
  );
3250
4175
  }
4176
+ const encryptFor = Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {};
4177
+ if (automergeMode) {
4178
+ const stored2 = {
4179
+ id,
4180
+ data: {},
4181
+ attachments: [],
4182
+ updatedAt: createdAt,
4183
+ isDeleted: false,
4184
+ decryptionKeyId: input.decryptionKeyId ?? "default"
4185
+ };
4186
+ persistMockAutomergeDocument(
4187
+ stored2,
4188
+ createMockAutomergeDocument(
4189
+ { ...input.set ?? {}, ...encryptFor },
4190
+ MOCK_AUTOMERGE_LOCAL_ACTOR
4191
+ )
4192
+ );
4193
+ storedDocuments.set(id, stored2);
4194
+ notifyLiveQueries();
4195
+ return {
4196
+ id,
4197
+ data: structuredClone(stored2.data),
4198
+ heads: [...stored2.heads ?? []],
4199
+ attachments: [],
4200
+ updatedAt: createdAt,
4201
+ ...mockDecryptionKeyId(stored2)
4202
+ };
4203
+ }
3251
4204
  const created = {
3252
4205
  id,
3253
4206
  data: {
3254
4207
  ...readMockTypedValues(input.set ?? {}),
3255
- ...Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {}
4208
+ ...encryptFor
3256
4209
  },
3257
4210
  heads: [`mock-head-${++changeCounter}`],
3258
4211
  attachments: [],
@@ -3283,6 +4236,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3283
4236
  async update(docId, patch) {
3284
4237
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3285
4238
  const existing = storedDocuments.get(docId);
4239
+ if (automergeMode) {
4240
+ const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR) : createMockAutomergeDocument(existing?.data ?? {}, MOCK_AUTOMERGE_LOCAL_ACTOR);
4241
+ return writeAutomergeDocument(docId, existing, applyMockAutomergeUpdate(current, patch));
4242
+ }
3286
4243
  const updated = {
3287
4244
  id: docId,
3288
4245
  data: applyDocumentUpdatePatch(existing?.data ?? {}, patch),
@@ -3369,7 +4326,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3369
4326
  storedDocuments.set(_docId, {
3370
4327
  ...existing,
3371
4328
  isDeleted: false,
3372
- heads: [`mock-head-${++changeCounter}`],
4329
+ heads: automergeMode ? existing.heads : [`mock-head-${++changeCounter}`],
3373
4330
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3374
4331
  });
3375
4332
  notifyLiveQueries();
@@ -3593,7 +4550,26 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3593
4550
  createdAt: document2.updatedAt ?? null
3594
4551
  })),
3595
4552
  documents: storedDocuments,
3596
- notifyLiveQueries: refreshLiveQueries
4553
+ notifyLiveQueries: refreshLiveQueries,
4554
+ async applyRemoteUpdate(docId, input, options) {
4555
+ const existing = storedDocuments.get(docId);
4556
+ if (!existing || existing.isDeleted) {
4557
+ throw new Error(`Document ${docId} was not found.`);
4558
+ }
4559
+ if (!automergeMode || !existing.automergeBinary) {
4560
+ return await defaultDocuments.update(docId, input);
4561
+ }
4562
+ const current = loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR);
4563
+ const remote = applyMockAutomergeUpdate(
4564
+ forkMockAutomergeDocument(
4565
+ current,
4566
+ input.json?.baseHeads,
4567
+ options?.actor ?? MOCK_AUTOMERGE_REMOTE_ACTOR
4568
+ ),
4569
+ input.json ? { ...input, json: { ...input.json, baseHeads: void 0 } } : input
4570
+ );
4571
+ return writeAutomergeDocument(docId, existing, Automerge.merge(current, remote));
4572
+ }
3597
4573
  };
3598
4574
  }
3599
4575
  function createMockSessionState(options = {}) {
@@ -3619,6 +4595,7 @@ function createMockSessionState(options = {}) {
3619
4595
  const databaseViewDocumentLists = /* @__PURE__ */ new Map();
3620
4596
  const databaseDocumentStores = /* @__PURE__ */ new Map();
3621
4597
  const databaseLiveQueryRefreshers = /* @__PURE__ */ new Map();
4598
+ const databaseRemoteUpdaters = /* @__PURE__ */ new Map();
3622
4599
  const databasePeers = {
3623
4600
  getDocuments: (databaseId) => databaseDocumentStores.get(databaseId),
3624
4601
  refreshAllLiveQueries: () => {
@@ -3709,6 +4686,7 @@ function createMockSessionState(options = {}) {
3709
4686
  databaseViewApis.clear();
3710
4687
  databaseDocumentStores.clear();
3711
4688
  databaseLiveQueryRefreshers.clear();
4689
+ databaseRemoteUpdaters.clear();
3712
4690
  databaseInfos = definitions.map((definition) => ({
3713
4691
  ...definition.info,
3714
4692
  capabilities: [...definition.info.capabilities]
@@ -3727,6 +4705,7 @@ function createMockSessionState(options = {}) {
3727
4705
  databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
3728
4706
  databaseDocumentStores.set(definition.info.id, created.documents);
3729
4707
  databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
4708
+ databaseRemoteUpdaters.set(definition.info.id, created.applyRemoteUpdate);
3730
4709
  databaseViewApis.set(definition.info.id, {
3731
4710
  async create(input) {
3732
4711
  const build = () => createEvaluatingViewNavigator(
@@ -3849,10 +4828,10 @@ function createMockSessionState(options = {}) {
3849
4828
  `Unknown test database for view creation: ${firstDatabaseId}`
3850
4829
  );
3851
4830
  }
3852
- const view = await api.create(input);
4831
+ const view2 = await api.create(input);
3853
4832
  const viewId = input.definition.id || crypto.randomUUID();
3854
- sessionViews.set(viewId, view);
3855
- return view;
4833
+ sessionViews.set(viewId, view2);
4834
+ return view2;
3856
4835
  },
3857
4836
  async openViewNavigator(viewId, options2) {
3858
4837
  const existing = sessionViews.get(viewId);
@@ -3860,7 +4839,7 @@ function createMockSessionState(options = {}) {
3860
4839
  return existing;
3861
4840
  }
3862
4841
  const sourceDatabaseId = launchContext.views.find(
3863
- (view2) => view2.id === viewId
4842
+ (view3) => view3.id === viewId
3864
4843
  )?.sources[0]?.databaseId;
3865
4844
  if (!sourceDatabaseId) {
3866
4845
  throw new Error(`Unknown test view: ${viewId}`);
@@ -3871,9 +4850,9 @@ function createMockSessionState(options = {}) {
3871
4850
  `Unknown test database for view ${viewId}: ${sourceDatabaseId}`
3872
4851
  );
3873
4852
  }
3874
- const view = await api.open(viewId, options2);
3875
- sessionViews.set(viewId, view);
3876
- return view;
4853
+ const view2 = await api.open(viewId, options2);
4854
+ sessionViews.set(viewId, view2);
4855
+ return view2;
3877
4856
  },
3878
4857
  menus,
3879
4858
  drag,
@@ -4010,6 +4989,13 @@ function createMockSessionState(options = {}) {
4010
4989
  }
4011
4990
  return database;
4012
4991
  },
4992
+ async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
4993
+ const applyRemoteUpdate = databaseRemoteUpdaters.get(databaseId);
4994
+ if (!applyRemoteUpdate) {
4995
+ throw new Error(`Unknown test database: ${databaseId}`);
4996
+ }
4997
+ return await applyRemoteUpdate(docId, input, updateOptions);
4998
+ },
4013
4999
  getLicensedProducts() {
4014
5000
  return [...launchContext.licensedProducts ?? []];
4015
5001
  },
@@ -4126,6 +5112,7 @@ function createMockMindooDBAppSession(options = {}) {
4126
5112
  setCapabilities: state.setCapabilities,
4127
5113
  setEnforceCapabilities: state.setEnforceCapabilities,
4128
5114
  setDirectoryUsers: state.setDirectoryUsers,
5115
+ applyRemoteUpdate: state.applyRemoteUpdate,
4129
5116
  emitThemeChange: state.emitThemeChange,
4130
5117
  emitViewportChange: state.emitViewportChange,
4131
5118
  emitUiPreferencesChange: state.emitUiPreferencesChange,
@@ -4158,6 +5145,20 @@ function createFakeBridgeHost(options = {}) {
4158
5145
  );
4159
5146
  const requests = [];
4160
5147
  const connectedPorts = /* @__PURE__ */ new Set();
5148
+ const agentHost = createMockAgentHost({
5149
+ ...options.agentToolPrefix !== void 0 ? { agentToolPrefix: options.agentToolPrefix } : {},
5150
+ ...options.agentToolsEnabled !== void 0 ? { agentToolsEnabled: options.agentToolsEnabled } : {},
5151
+ ...options.agentConsent !== void 0 ? { agentConsent: options.agentConsent } : {},
5152
+ ...options.agentInvokeTimeoutMs !== void 0 ? { agentInvokeTimeoutMs: options.agentInvokeTimeoutMs } : {},
5153
+ appId: () => state.getLaunchContext().appId,
5154
+ post(message) {
5155
+ const port = [...connectedPorts].pop();
5156
+ if (!port) return false;
5157
+ port.postMessage(message);
5158
+ return true;
5159
+ },
5160
+ onChange: () => options.onAgentChange?.()
5161
+ });
4161
5162
  state.embedHost.onClosed((event) => {
4162
5163
  connectedPorts.forEach(
4163
5164
  (port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "closed", ...event } })
@@ -4199,6 +5200,7 @@ function createFakeBridgeHost(options = {}) {
4199
5200
  "Expected the bridge connection to transfer a MessagePort."
4200
5201
  );
4201
5202
  }
5203
+ if (connectedPorts.size > 0) agentHost.reset("The app was reloaded.");
4202
5204
  connectedPorts.add(port);
4203
5205
  port.addEventListener("message", (event) => {
4204
5206
  void handlePortMessage(port, event.data);
@@ -4289,9 +5291,9 @@ function createFakeBridgeHost(options = {}) {
4289
5291
  if (existing) {
4290
5292
  return existing;
4291
5293
  }
4292
- const navigator = await state.openViewNavigator(viewId);
4293
- viewSessions.set(viewId, navigator);
4294
- return navigator;
5294
+ const navigator2 = await state.openViewNavigator(viewId);
5295
+ viewSessions.set(viewId, navigator2);
5296
+ return navigator2;
4295
5297
  }
4296
5298
  function postRpcSuccess(port, requestId, result) {
4297
5299
  port.postMessage({
@@ -4340,14 +5342,14 @@ function createFakeBridgeHost(options = {}) {
4340
5342
  return { ok: true };
4341
5343
  case "session.createViewNavigator": {
4342
5344
  const navigatorId = `navigator-${viewCounter += 1}`;
4343
- const navigator = await state.createViewNavigator({
5345
+ const navigator2 = await state.createViewNavigator({
4344
5346
  databaseIds: Array.isArray(params.databaseIds) ? params.databaseIds.map((entry) => String(entry)) : [],
4345
5347
  definition: params.definition,
4346
5348
  categorizationStyle: params.categorizationStyle === "category_then_document" ? "category_then_document" : "document_then_category",
4347
5349
  options: params.options
4348
5350
  });
4349
- viewSessions.set(navigatorId, navigator);
4350
- navigator.onDidUpdate((stats) => {
5351
+ viewSessions.set(navigatorId, navigator2);
5352
+ navigator2.onDidUpdate((stats) => {
4351
5353
  if (!viewSessions.has(navigatorId)) return;
4352
5354
  const payload = {
4353
5355
  protocol: PROTOCOL,
@@ -4361,11 +5363,11 @@ function createFakeBridgeHost(options = {}) {
4361
5363
  }
4362
5364
  case "session.openViewNavigator": {
4363
5365
  const navigatorId = `navigator-${viewCounter += 1}`;
4364
- const navigator = await state.openViewNavigator(
5366
+ const navigator2 = await state.openViewNavigator(
4365
5367
  String(params.viewId),
4366
5368
  params.options
4367
5369
  );
4368
- viewSessions.set(navigatorId, navigator);
5370
+ viewSessions.set(navigatorId, navigator2);
4369
5371
  return { navigatorId };
4370
5372
  }
4371
5373
  case "menus.show":
@@ -4858,8 +5860,8 @@ function createFakeBridgeHost(options = {}) {
4858
5860
  );
4859
5861
  case "viewNavigators.dispose": {
4860
5862
  const key = String(params.navigatorId);
4861
- const navigator = await resolveViewNavigator(key);
4862
- await navigator.dispose();
5863
+ const navigator2 = await resolveViewNavigator(key);
5864
+ await navigator2.dispose();
4863
5865
  viewSessions.delete(key);
4864
5866
  return void 0;
4865
5867
  }
@@ -4944,6 +5946,10 @@ function createFakeBridgeHost(options = {}) {
4944
5946
  beforeCloseWaiters.get(message.closeId)?.();
4945
5947
  return;
4946
5948
  }
5949
+ if (message.kind === "agent-result") {
5950
+ agentHost.handleResult(message);
5951
+ return;
5952
+ }
4947
5953
  if (message.kind === "workspace-focus-requested") {
4948
5954
  state.setHostFocused(true);
4949
5955
  const payload = {
@@ -4985,6 +5991,11 @@ function createFakeBridgeHost(options = {}) {
4985
5991
  succeed(result);
4986
5992
  return;
4987
5993
  }
5994
+ const agentResult = await agentHost.dispatch(message.method, message.params);
5995
+ if (agentResult) {
5996
+ succeed(agentResult.result);
5997
+ return;
5998
+ }
4988
5999
  const builtinResult = await handleBuiltinRequest(message);
4989
6000
  if (builtinResult === NOT_HANDLED) {
4990
6001
  fail({
@@ -5026,6 +6037,7 @@ function createFakeBridgeHost(options = {}) {
5026
6037
  const controller = {
5027
6038
  bridge: state.bridge,
5028
6039
  session: state.session,
6040
+ agent: agentHost.controller,
5029
6041
  get launchId() {
5030
6042
  return state.getLaunchContext().launchId;
5031
6043
  },
@@ -5043,6 +6055,7 @@ function createFakeBridgeHost(options = {}) {
5043
6055
  getEnforceCapabilities: state.getEnforceCapabilities,
5044
6056
  setDirectoryUsers: state.setDirectoryUsers,
5045
6057
  getDirectoryUsers: state.getDirectoryUsers,
6058
+ applyRemoteUpdate: state.applyRemoteUpdate,
5046
6059
  listEmbeds: () => state.embedHost.listEmbeds(),
5047
6060
  closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
5048
6061
  getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
@@ -5183,12 +6196,15 @@ export {
5183
6196
  createEvaluatingViewNavigator,
5184
6197
  createFakeBridgeHost,
5185
6198
  createMemoryAttachments,
6199
+ createMockAgentHost,
5186
6200
  createMockMindooDBAppBridge,
5187
6201
  createMockMindooDBAppSession,
5188
6202
  generateDirectoryUsers,
5189
6203
  loadMindoodbForTesting,
6204
+ mockAgentToolPrefix,
5190
6205
  mockDatabasesFromDefinition,
5191
6206
  mountHavenTestHost,
5192
6207
  previewJson,
5193
- readTestHostUrlSettings
6208
+ readTestHostUrlSettings,
6209
+ validateMockAgentTools
5194
6210
  };