@agent-native/core 0.81.3 → 0.83.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.
Files changed (92) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/CommandMenu.tsx +34 -12
  5. package/corpus/core/src/file-upload/builder.ts +199 -37
  6. package/corpus/core/src/file-upload/index.ts +2 -0
  7. package/corpus/core/src/file-upload/types.ts +38 -0
  8. package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
  9. package/corpus/templates/clips/actions/create-recording.ts +44 -0
  10. package/corpus/templates/clips/actions/finalize-recording.ts +198 -88
  11. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +12 -0
  12. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +83 -29
  13. package/corpus/templates/clips/app/routes/record.tsx +12 -1
  14. package/corpus/templates/clips/server/lib/resumable-session.ts +39 -0
  15. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +2 -0
  16. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +186 -29
  17. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +4 -0
  18. package/corpus/templates/clips/shared/recording-core.ts +5 -0
  19. package/corpus/templates/content/.env.local.example +6 -0
  20. package/corpus/templates/content/AGENTS.md +10 -1
  21. package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
  22. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
  23. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
  24. package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
  25. package/corpus/templates/content/actions/_database-utils.ts +41 -0
  26. package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
  27. package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
  28. package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
  29. package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
  30. package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
  31. package/corpus/templates/content/actions/list-content-databases.ts +41 -33
  32. package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
  33. package/corpus/templates/content/actions/update-document.ts +93 -0
  34. package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
  35. package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
  36. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +105 -17
  37. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
  38. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
  39. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
  40. package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
  41. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
  42. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +28 -33
  43. package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +47 -0
  44. package/corpus/templates/content/app/global.css +10 -3
  45. package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
  46. package/corpus/templates/content/app/hooks/use-documents.ts +37 -0
  47. package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
  48. package/corpus/templates/content/app/i18n-data.ts +212 -0
  49. package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
  50. package/corpus/templates/content/app/root.tsx +236 -8
  51. package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
  52. package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
  53. package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
  54. package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
  55. package/corpus/templates/content/changelog/2026-06-30-sidebar-favorites-now-keep-long-titles-tidy-and-stay-in-sync.md +6 -0
  56. package/corpus/templates/content/package.json +3 -1
  57. package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
  58. package/corpus/templates/content/scripts/dev-database.mjs +83 -0
  59. package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
  60. package/corpus/templates/content/server/db/schema.ts +27 -0
  61. package/corpus/templates/content/server/plugins/db.ts +33 -0
  62. package/corpus/templates/content/shared/api.ts +45 -0
  63. package/corpus/templates/content/shared/builder-mdx.ts +905 -5
  64. package/corpus/templates/content/shared/nfm-registry.ts +2 -0
  65. package/corpus/templates/content/shared/source-component-block.ts +141 -0
  66. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
  67. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
  68. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
  69. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
  70. package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
  71. package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
  72. package/dist/client/CommandMenu.d.ts +6 -2
  73. package/dist/client/CommandMenu.d.ts.map +1 -1
  74. package/dist/client/CommandMenu.js +25 -14
  75. package/dist/client/CommandMenu.js.map +1 -1
  76. package/dist/collab/awareness.d.ts +2 -2
  77. package/dist/collab/awareness.d.ts.map +1 -1
  78. package/dist/collab/routes.d.ts +1 -1
  79. package/dist/file-upload/builder.d.ts.map +1 -1
  80. package/dist/file-upload/builder.js +137 -25
  81. package/dist/file-upload/builder.js.map +1 -1
  82. package/dist/file-upload/index.d.ts +1 -1
  83. package/dist/file-upload/index.d.ts.map +1 -1
  84. package/dist/file-upload/index.js.map +1 -1
  85. package/dist/file-upload/types.d.ts +26 -0
  86. package/dist/file-upload/types.d.ts.map +1 -1
  87. package/dist/file-upload/types.js.map +1 -1
  88. package/dist/notifications/routes.d.ts +3 -3
  89. package/dist/progress/routes.d.ts +1 -1
  90. package/dist/resources/handlers.d.ts +3 -3
  91. package/dist/server/transcribe-voice.d.ts +1 -1
  92. package/package.json +1 -1
@@ -6,6 +6,7 @@ import {
6
6
  parseDocumentHideFromSearch,
7
7
  } from "../server/lib/documents.js";
8
8
  import type {
9
+ ContentDatabaseBodyHydration,
9
10
  ContentDatabaseMembership,
10
11
  ContentDatabaseResponse,
11
12
  } from "../shared/api.js";
@@ -28,8 +29,27 @@ function canManageRole(role: string) {
28
29
  type DatabaseMembershipRow = {
29
30
  item: typeof schema.contentDatabaseItems.$inferSelect;
30
31
  database: typeof schema.contentDatabases.$inferSelect;
32
+ sourceId?: string | null;
31
33
  };
32
34
 
35
+ export function serializeBodyHydration(
36
+ item: typeof schema.contentDatabaseItems.$inferSelect,
37
+ ): ContentDatabaseBodyHydration {
38
+ const status = item.bodyHydrationStatus;
39
+ return {
40
+ status:
41
+ status === "pending" ||
42
+ status === "hydrating" ||
43
+ status === "hydrated" ||
44
+ status === "error"
45
+ ? status
46
+ : "hydrated",
47
+ attemptedAt: item.bodyHydrationAttemptedAt,
48
+ error: item.bodyHydrationError,
49
+ version: item.bodyHydrationVersion,
50
+ };
51
+ }
52
+
33
53
  export function serializeDatabaseMembership(
34
54
  row: DatabaseMembershipRow,
35
55
  ): ContentDatabaseMembership {
@@ -38,6 +58,8 @@ export function serializeDatabaseMembership(
38
58
  databaseDocumentId: row.database.documentId,
39
59
  databaseTitle: row.database.title || "Untitled database",
40
60
  position: row.item.position,
61
+ sourceId: row.sourceId ?? null,
62
+ bodyHydration: serializeBodyHydration(row.item),
41
63
  };
42
64
  }
43
65
 
@@ -178,6 +200,7 @@ export async function getContentDatabaseResponse(
178
200
  databaseId: item.databaseId,
179
201
  document: serializeDocument(document, { item, database }),
180
202
  position: item.position,
203
+ bodyHydration: serializeBodyHydration(item),
181
204
  properties: await listPropertiesForDatabase(databaseId, document),
182
205
  });
183
206
  }
@@ -288,12 +311,20 @@ export async function getDatabaseItemByDocumentId(
288
311
  .select({
289
312
  item: schema.contentDatabaseItems,
290
313
  database: schema.contentDatabases,
314
+ sourceId: schema.contentDatabaseSourceRows.sourceId,
291
315
  })
292
316
  .from(schema.contentDatabaseItems)
293
317
  .innerJoin(
294
318
  schema.contentDatabases,
295
319
  eq(schema.contentDatabases.id, schema.contentDatabaseItems.databaseId),
296
320
  )
321
+ .leftJoin(
322
+ schema.contentDatabaseSourceRows,
323
+ eq(
324
+ schema.contentDatabaseSourceRows.databaseItemId,
325
+ schema.contentDatabaseItems.id,
326
+ ),
327
+ )
297
328
  .where(and(...clauses));
298
329
  return row ?? null;
299
330
  }
@@ -327,6 +358,11 @@ export async function deleteDatabaseDataForDocument(
327
358
  .from(schema.contentDatabaseSources)
328
359
  .where(eq(schema.contentDatabaseSources.databaseId, database.id));
329
360
  for (const source of sources) {
361
+ await db
362
+ .delete(schema.contentDatabaseBodyHydrationQueue)
363
+ .where(
364
+ eq(schema.contentDatabaseBodyHydrationQueue.sourceId, source.id),
365
+ );
330
366
  await db
331
367
  .delete(schema.contentDatabaseSourceExecutions)
332
368
  .where(eq(schema.contentDatabaseSourceExecutions.sourceId, source.id));
@@ -363,6 +399,11 @@ export async function deleteDatabaseDataForDocument(
363
399
  includeDeleted: true,
364
400
  });
365
401
  if (item) {
402
+ await db
403
+ .delete(schema.contentDatabaseBodyHydrationQueue)
404
+ .where(
405
+ eq(schema.contentDatabaseBodyHydrationQueue.documentId, documentId),
406
+ );
366
407
  await db
367
408
  .delete(schema.documentPropertyValues)
368
409
  .where(
@@ -225,8 +225,71 @@ function folderDocumentsForFiles(files: LocalArtifactFileMeta[]) {
225
225
  .map((folderPath, index) => documentFromFolder(folderPath, index));
226
226
  }
227
227
 
228
+ let localFileDocumentsCache: {
229
+ scope: string;
230
+ signature: string;
231
+ documents: Document[];
232
+ cachedAt: number;
233
+ } | null = null;
234
+ const LOCAL_FILE_DOCUMENTS_CACHE_TTL_MS = 5_000;
235
+
236
+ function localFileDocumentsCacheScope() {
237
+ return JSON.stringify({
238
+ manifest: process.env.AGENT_NATIVE_MANIFEST,
239
+ manifestPath: process.env.AGENT_NATIVE_MANIFEST_PATH,
240
+ mode: process.env.AGENT_NATIVE_MODE,
241
+ dataMode: process.env.AGENT_NATIVE_DATA_MODE,
242
+ });
243
+ }
244
+
245
+ function localFileDocumentsSignature(files: LocalArtifactFileMeta[]) {
246
+ return JSON.stringify(
247
+ files.map((file) => ({
248
+ absolutePath: file.absolutePath,
249
+ contentType: file.contentType,
250
+ hash: file.hash,
251
+ path: file.path,
252
+ profile: file.profile,
253
+ rootName: file.rootName,
254
+ rootPath: file.rootPath,
255
+ sizeBytes: file.sizeBytes,
256
+ updatedAt: file.updatedAt,
257
+ })),
258
+ );
259
+ }
260
+
261
+ function cloneLocalDocument(document: Document): Document {
262
+ return {
263
+ ...document,
264
+ source: document.source ? { ...document.source } : undefined,
265
+ };
266
+ }
267
+
268
+ function invalidateLocalFileDocumentsCache() {
269
+ localFileDocumentsCache = null;
270
+ }
271
+
228
272
  export async function listLocalFileDocuments(): Promise<Document[]> {
273
+ const scope = localFileDocumentsCacheScope();
274
+ if (
275
+ localFileDocumentsCache &&
276
+ localFileDocumentsCache.scope === scope &&
277
+ Date.now() - localFileDocumentsCache.cachedAt <
278
+ LOCAL_FILE_DOCUMENTS_CACHE_TTL_MS
279
+ ) {
280
+ return localFileDocumentsCache.documents.map(cloneLocalDocument);
281
+ }
282
+
229
283
  const files = await listLocalArtifactFiles(localOptions());
284
+ const signature = localFileDocumentsSignature(files);
285
+ if (
286
+ localFileDocumentsCache?.scope === scope &&
287
+ localFileDocumentsCache.signature === signature
288
+ ) {
289
+ localFileDocumentsCache.cachedAt = Date.now();
290
+ return localFileDocumentsCache.documents.map(cloneLocalDocument);
291
+ }
292
+
230
293
  const folderDocuments = folderDocumentsForFiles(files);
231
294
  const fileDocuments = await Promise.all(
232
295
  files.map(async (meta, index) => {
@@ -237,7 +300,14 @@ export async function listLocalFileDocuments(): Promise<Document[]> {
237
300
  return documentFromLocalFile(file ?? meta, file?.content ?? "", index);
238
301
  }),
239
302
  );
240
- return [...folderDocuments, ...fileDocuments];
303
+ const documents = [...folderDocuments, ...fileDocuments];
304
+ localFileDocumentsCache = {
305
+ scope,
306
+ signature,
307
+ documents,
308
+ cachedAt: Date.now(),
309
+ };
310
+ return documents.map(cloneLocalDocument);
241
311
  }
242
312
 
243
313
  export async function getLocalFileDocument(id: string): Promise<Document> {
@@ -442,6 +512,7 @@ export async function updateLocalFileDocument(
442
512
  content: nextSource,
443
513
  expectedHash: file.hash,
444
514
  });
515
+ invalidateLocalFileDocumentsCache();
445
516
  return getLocalFileDocument(id);
446
517
  }
447
518
 
@@ -505,6 +576,7 @@ export async function createLocalFileDocument(
505
576
  content: source,
506
577
  ifNotExists: true,
507
578
  });
579
+ invalidateLocalFileDocumentsCache();
508
580
  return getLocalFileDocument(localFileDocumentId(path));
509
581
  } catch (error) {
510
582
  if (isAlreadyExistsError(error)) continue;
@@ -521,6 +593,7 @@ export async function deleteLocalFileDocument(id: string) {
521
593
  }
522
594
  const path = localDocumentPathFromId(id);
523
595
  await deleteLocalArtifactFile({ ...localOptions(), path });
596
+ invalidateLocalFileDocumentsCache();
524
597
  return { success: true, deleted: 1 };
525
598
  }
526
599
 
@@ -16,6 +16,7 @@ import {
16
16
  import type { BuilderCmsSourceEntry } from "./_builder-cms-source-adapter.js";
17
17
  import {
18
18
  databaseSourceExistsForTable,
19
+ enqueueBuilderBodyHydrationForItems,
19
20
  ensureDatabaseSourceProperty,
20
21
  getExistingSource,
21
22
  getSourceRows,
@@ -265,6 +266,8 @@ export default defineAction({
265
266
  const additionalRead = await readBuilderCmsContentEntries({
266
267
  model: sourceTable,
267
268
  });
269
+ const additionalEntries =
270
+ additionalRead.state === "live" ? additionalRead.entries : [];
268
271
  const additionalModelFields = await readBuilderCmsModelFields({
269
272
  model: sourceTable,
270
273
  });
@@ -284,7 +287,7 @@ export default defineAction({
284
287
  if (additionalRead.state === "live") {
285
288
  await importBuilderCmsEntriesAsDatabaseItems({
286
289
  database,
287
- entries: additionalRead.entries,
290
+ entries: additionalEntries,
288
291
  now,
289
292
  sourceTable,
290
293
  existingSourceRows: [],
@@ -299,7 +302,7 @@ export default defineAction({
299
302
  const additionalEntriesByDocumentId =
300
303
  additionalRead.state === "live"
301
304
  ? mapBuilderCmsEntriesToLocalItems({
302
- entries: additionalRead.entries,
305
+ entries: additionalEntries,
303
306
  items: importedItems,
304
307
  sourceTable,
305
308
  now,
@@ -312,8 +315,7 @@ export default defineAction({
312
315
  sourceType,
313
316
  properties: additionalSetup.properties,
314
317
  builderModelFields: additionalModelFields,
315
- builderSampleEntries:
316
- additionalRead.state === "live" ? additionalRead.entries : [],
318
+ builderSampleEntries: additionalEntries,
317
319
  now,
318
320
  });
319
321
  await seedMockSourceRows({
@@ -325,6 +327,17 @@ export default defineAction({
325
327
  now,
326
328
  builderEntriesByDocumentId: additionalEntriesByDocumentId,
327
329
  });
330
+ if (additionalRead.state === "live") {
331
+ await enqueueBuilderBodyHydrationForItems({
332
+ sourceId: additionalSourceId,
333
+ ownerEmail: database.ownerEmail,
334
+ orgId: database.orgId,
335
+ sourceTable,
336
+ items: importedItems,
337
+ builderEntriesByDocumentId: additionalEntriesByDocumentId,
338
+ now,
339
+ });
340
+ }
328
341
  await updateBuilderCmsSourceReadMetadata({
329
342
  sourceId: additionalSourceId,
330
343
  sourceTable,
@@ -365,6 +378,8 @@ export default defineAction({
365
378
  model: sourceTable,
366
379
  })
367
380
  : null;
381
+ const builderEntries =
382
+ builderRead?.state === "live" ? builderRead.entries : [];
368
383
  const builderModelFields =
369
384
  sourceType === "builder-cms"
370
385
  ? await readBuilderCmsModelFields({
@@ -374,7 +389,7 @@ export default defineAction({
374
389
  if (builderRead?.state === "live") {
375
390
  await importBuilderCmsEntriesAsDatabaseItems({
376
391
  database,
377
- entries: builderRead.entries,
392
+ entries: builderEntries,
378
393
  now,
379
394
  sourceTable,
380
395
  existingSourceRows,
@@ -385,7 +400,7 @@ export default defineAction({
385
400
  const builderEntriesByDocumentId =
386
401
  builderRead?.state === "live"
387
402
  ? mapBuilderCmsEntriesToLocalItems({
388
- entries: builderRead.entries,
403
+ entries: builderEntries,
389
404
  items: refreshedSetup.response.items,
390
405
  sourceTable,
391
406
  now,
@@ -399,8 +414,7 @@ export default defineAction({
399
414
  sourceType,
400
415
  properties: refreshedSetup.properties,
401
416
  builderModelFields,
402
- builderSampleEntries:
403
- builderRead?.state === "live" ? builderRead.entries : [],
417
+ builderSampleEntries: builderEntries,
404
418
  now,
405
419
  });
406
420
  await seedMockSourceRows({
@@ -412,6 +426,17 @@ export default defineAction({
412
426
  now,
413
427
  builderEntriesByDocumentId,
414
428
  });
429
+ if (sourceType === "builder-cms" && builderRead?.state === "live") {
430
+ await enqueueBuilderBodyHydrationForItems({
431
+ sourceId,
432
+ ownerEmail: database.ownerEmail,
433
+ orgId: database.orgId,
434
+ sourceTable,
435
+ items: refreshedSetup.response.items,
436
+ builderEntriesByDocumentId,
437
+ now,
438
+ });
439
+ }
415
440
  if (sourceType === "builder-cms" && builderRead) {
416
441
  await updateBuilderCmsSourceReadMetadata({
417
442
  sourceId,
@@ -17,6 +17,7 @@ import {
17
17
  } from "./_builder-cms-read-client.js";
18
18
  import type { BuilderCmsSourceEntry } from "./_builder-cms-source-adapter.js";
19
19
  import {
20
+ enqueueBuilderBodyHydrationForItems,
20
21
  ensureDatabaseSourceProperty,
21
22
  importBuilderCmsEntriesAsDatabaseItems,
22
23
  mapBuilderCmsEntriesToLocalItems,
@@ -383,6 +384,19 @@ export default defineAction({
383
384
  now,
384
385
  builderEntriesByDocumentId,
385
386
  });
387
+ if (read.state === "live") {
388
+ await enqueueBuilderBodyHydrationForItems({
389
+ sourceId: source.id,
390
+ ownerEmail: database.ownerEmail,
391
+ orgId: database.orgId,
392
+ sourceTable: source.sourceTable,
393
+ items: refreshedSetup.response.items.filter((item) =>
394
+ builderEntriesByDocumentId.has(item.document.id),
395
+ ),
396
+ builderEntriesByDocumentId,
397
+ now,
398
+ });
399
+ }
386
400
  await updateBuilderCmsSourceReadMetadata({
387
401
  sourceId: source.id,
388
402
  sourceTable: source.sourceTable,
@@ -16,6 +16,9 @@ import { getContentDatabaseResponse } from "./_database-utils.js";
16
16
 
17
17
  async function deleteSourceRecords(sourceId: string) {
18
18
  const db = getDb();
19
+ await db
20
+ .delete(schema.contentDatabaseBodyHydrationQueue)
21
+ .where(eq(schema.contentDatabaseBodyHydrationQueue.sourceId, sourceId));
19
22
  await db
20
23
  .delete(schema.contentDatabaseSourceExecutions)
21
24
  .where(eq(schema.contentDatabaseSourceExecutions.sourceId, sourceId));
@@ -11,13 +11,19 @@ import {
11
11
  type ContentDatabaseSourceChangeSet,
12
12
  type ContentDatabaseSourceExecutionState,
13
13
  type ContentDatabaseSourcePushMode,
14
+ type DocumentPropertyValue,
14
15
  type ExecuteBuilderSourceExecutionRequest,
15
16
  } from "../shared/api.js";
16
17
  import {
17
18
  type BuilderCmsEntryLiveState,
18
19
  readBuilderCmsEntryLiveState,
19
20
  } from "./_builder-cms-read-client.js";
20
- import { builderCmsQualifiedId } from "./_builder-cms-source-adapter.js";
21
+ import {
22
+ BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
23
+ BUILDER_CMS_BODY_CONTENT_KEY,
24
+ BUILDER_CMS_BODY_SIDECARS_KEY,
25
+ builderCmsQualifiedId,
26
+ } from "./_builder-cms-source-adapter.js";
21
27
  import type {
22
28
  BuilderCmsExecutionPayload,
23
29
  BuilderCmsExecutionPlan,
@@ -255,6 +261,47 @@ function builderCmsAuthoritativeLastUpdated(
255
261
  );
256
262
  }
257
263
 
264
+ function parseSourceValues(
265
+ value: string | null | undefined,
266
+ ): Record<string, DocumentPropertyValue> {
267
+ if (!value) return {};
268
+ try {
269
+ const parsed = JSON.parse(value) as unknown;
270
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
271
+ ? (parsed as Record<string, DocumentPropertyValue>)
272
+ : {};
273
+ } catch {
274
+ return {};
275
+ }
276
+ }
277
+
278
+ function builderCmsReconciledSourceValuesJson(args: {
279
+ existingSourceValuesJson: string | null | undefined;
280
+ snapshotSourceValues: Record<string, DocumentPropertyValue> | undefined;
281
+ changeSet: ContentDatabaseSourceChangeSet;
282
+ }) {
283
+ const next = {
284
+ ...(args.snapshotSourceValues ?? {}),
285
+ ...parseSourceValues(args.existingSourceValuesJson),
286
+ };
287
+ for (const field of args.changeSet.fieldChanges) {
288
+ next[field.sourceFieldKey] = field.proposedValue;
289
+ }
290
+ const bodyChange = args.changeSet.bodyChange;
291
+ if (bodyChange) {
292
+ if (bodyChange.proposedHash !== undefined) {
293
+ next[BUILDER_CMS_BODY_BLOCKS_HASH_KEY] = bodyChange.proposedHash;
294
+ }
295
+ if (bodyChange.proposedContent !== undefined) {
296
+ next[BUILDER_CMS_BODY_CONTENT_KEY] = bodyChange.proposedContent;
297
+ }
298
+ if (bodyChange.sidecarsJson !== undefined) {
299
+ next[BUILDER_CMS_BODY_SIDECARS_KEY] = bodyChange.sidecarsJson;
300
+ }
301
+ }
302
+ return JSON.stringify(next);
303
+ }
304
+
258
305
  function sourceRowForChangeSet(
259
306
  source: ContentDatabaseSource,
260
307
  changeSet: ContentDatabaseSourceChangeSet,
@@ -270,6 +317,7 @@ function sourceRowForChangeSet(
270
317
 
271
318
  function requiresLivePreflight(effect: BuilderCmsExecutionPayload["effect"]) {
272
319
  return (
320
+ effect === "autosave" ||
273
321
  effect === "update_in_place" ||
274
322
  effect === "publish" ||
275
323
  effect === "unpublish"
@@ -314,6 +362,7 @@ function liveTimestampsDiffer(args: {
314
362
  function livePreflightBlockMessage(args: {
315
363
  liveState: BuilderCmsEntryLiveState;
316
364
  baselineLastUpdated: string | null;
365
+ baselineBlocksHash: string | null;
317
366
  effect: BuilderCmsExecutionPayload["effect"];
318
367
  }) {
319
368
  if (!args.liveState.exists) {
@@ -327,6 +376,13 @@ function livePreflightBlockMessage(args: {
327
376
  ) {
328
377
  return "Builder entry changed since this diff was approved; refresh and re-review.";
329
378
  }
379
+ if (
380
+ args.baselineBlocksHash &&
381
+ args.liveState.blocksHash &&
382
+ args.baselineBlocksHash !== args.liveState.blocksHash
383
+ ) {
384
+ return "Builder body changed since this diff was approved; refresh and re-review.";
385
+ }
330
386
  if (args.effect === "publish" && args.liveState.published !== "draft") {
331
387
  return "Entry is already published.";
332
388
  }
@@ -415,10 +471,20 @@ async function reconcileBuilderCmsWrite(args: {
415
471
  : [];
416
472
 
417
473
  const [row] = existingRow;
474
+ const snapshotRow = sourceRowForChangeSet(args.source, args.changeSet);
475
+ const sourceValuesJson = builderCmsReconciledSourceValuesJson({
476
+ existingSourceValuesJson: row?.sourceValuesJson,
477
+ snapshotSourceValues: snapshotRow?.sourceValues,
478
+ changeSet: args.changeSet,
479
+ });
480
+ const patchWithValues = {
481
+ ...patch,
482
+ sourceValuesJson,
483
+ };
418
484
  if (row) {
419
485
  await db
420
486
  .update(schema.contentDatabaseSourceRows)
421
- .set(patch)
487
+ .set(patchWithValues)
422
488
  .where(eq(schema.contentDatabaseSourceRows.id, row.id));
423
489
  } else if (args.changeSet.documentId && args.changeSet.databaseItemId) {
424
490
  await db.insert(schema.contentDatabaseSourceRows).values({
@@ -428,7 +494,7 @@ async function reconcileBuilderCmsWrite(args: {
428
494
  databaseItemId: args.changeSet.databaseItemId,
429
495
  documentId: args.changeSet.documentId,
430
496
  createdAt: args.now,
431
- ...patch,
497
+ ...patchWithValues,
432
498
  });
433
499
  } else {
434
500
  throw new Error(
@@ -763,6 +829,7 @@ export async function executeBuilderSourceExecutionWithDeps(
763
829
  const message = livePreflightBlockMessage({
764
830
  liveState,
765
831
  baselineLastUpdated: targetRow?.lastSourceUpdatedAt ?? null,
832
+ baselineBlocksHash: changeSet.bodyChange?.currentHash ?? null,
766
833
  effect: plan.payload.effect,
767
834
  });
768
835
  if (message) {
@@ -777,6 +844,7 @@ export async function executeBuilderSourceExecutionWithDeps(
777
844
  exists: liveState.exists,
778
845
  published: liveState.published,
779
846
  lastUpdated: liveState.lastUpdated,
847
+ blocksHash: liveState.blocksHash,
780
848
  id: liveState.id,
781
849
  },
782
850
  },
@@ -1,70 +1,78 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, asc, inArray, isNull } from "drizzle-orm";
3
+ import { and, asc, eq, isNull, ne, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
7
  import { documentDiscoveryFilter } from "../server/lib/documents.js";
8
8
  import type { ListContentDatabasesResponse } from "../shared/api.js";
9
9
 
10
+ function escapeLike(s: string): string {
11
+ return s.replace(/([\\%_])/g, "\\$1");
12
+ }
13
+
10
14
  export default defineAction({
11
15
  description:
12
- "List the content databases the user can access (owned, shared, or org-shared — matching the sidebar) so any of them can be used as a local-table source. Optionally excludes one database (e.g. the one being configured).",
16
+ "List the content databases the user can access (owned, shared, or org-shared — matching the sidebar) so any of them can be used as a local-table source. Optionally filters by title or excludes one database (e.g. the one being configured).",
13
17
  schema: z.object({
14
18
  excludeDatabaseId: z
15
19
  .string()
16
20
  .optional()
17
21
  .describe("Database id to omit from the results."),
22
+ query: z.string().optional().describe("Optional title search text."),
23
+ limit: z.coerce
24
+ .number()
25
+ .int()
26
+ .min(1)
27
+ .max(50)
28
+ .optional()
29
+ .describe("Maximum number of databases to return."),
18
30
  }),
19
31
  http: { method: "GET" },
20
32
  readOnly: true,
21
33
  run: async (args): Promise<ListContentDatabasesResponse> => {
22
34
  const db = getDb();
35
+ const query = args.query?.trim();
36
+ const pattern = query ? `%${escapeLike(query.toLowerCase())}%` : null;
23
37
  // The same access + discovery filter the sidebar uses, so the picker shows
24
38
  // owned AND shared/org databases and never a trashed/hidden one.
25
- const accessibleDocs = await db
39
+ const queryBuilder = db
26
40
  .select({
27
- id: schema.documents.id,
41
+ id: schema.contentDatabases.id,
42
+ documentId: schema.contentDatabases.documentId,
28
43
  title: schema.documents.title,
29
44
  })
30
- .from(schema.documents)
45
+ .from(schema.contentDatabases)
46
+ .innerJoin(
47
+ schema.documents,
48
+ eq(schema.contentDatabases.documentId, schema.documents.id),
49
+ )
31
50
  .where(
32
51
  and(
33
52
  accessFilter(schema.documents, schema.documentShares),
34
53
  documentDiscoveryFilter(),
54
+ isNull(schema.contentDatabases.deletedAt),
55
+ args.excludeDatabaseId
56
+ ? ne(schema.contentDatabases.id, args.excludeDatabaseId)
57
+ : undefined,
58
+ pattern
59
+ ? sql`lower(${schema.documents.title}) LIKE ${pattern} ESCAPE '\\'`
60
+ : undefined,
35
61
  ),
36
62
  )
37
63
  .orderBy(asc(schema.documents.position));
38
- if (accessibleDocs.length === 0) return { databases: [] };
39
64
 
40
- const titleByDocId = new Map(
41
- accessibleDocs.map((doc) => [doc.id, doc.title]),
42
- );
43
- const rows = await db
44
- .select({
45
- id: schema.contentDatabases.id,
46
- documentId: schema.contentDatabases.documentId,
47
- })
48
- .from(schema.contentDatabases)
49
- .where(
50
- and(
51
- inArray(
52
- schema.contentDatabases.documentId,
53
- accessibleDocs.map((doc) => doc.id),
54
- ),
55
- isNull(schema.contentDatabases.deletedAt),
56
- ),
57
- );
65
+ const rows = args.limit
66
+ ? await queryBuilder.limit(args.limit)
67
+ : await queryBuilder;
58
68
 
59
- const databases = rows
60
- .filter((row) => row.id !== args.excludeDatabaseId)
61
- .map((row) => ({
62
- databaseId: row.id,
63
- documentId: row.documentId,
64
- // The document's live title (matches the sidebar) rather than the
65
- // possibly-stale content_databases.title.
66
- title: titleByDocId.get(row.documentId) ?? "Untitled database",
67
- }));
69
+ const databases = rows.map((row) => ({
70
+ databaseId: row.id,
71
+ documentId: row.documentId,
72
+ // The document's live title (matches the sidebar) rather than the
73
+ // possibly-stale content_databases.title.
74
+ title: row.title ?? "Untitled database",
75
+ }));
68
76
 
69
77
  return { databases };
70
78
  },
@@ -0,0 +1,47 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { assertAccess } from "@agent-native/core/sharing";
3
+ import { eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import type {
8
+ ProcessBuilderBodyHydrationRequest,
9
+ ProcessBuilderBodyHydrationResponse,
10
+ } from "../shared/api.js";
11
+ import { processBuilderBodyHydrationQueue } from "./_database-source-utils.js";
12
+
13
+ export default defineAction({
14
+ description: "Hydrate queued Builder CMS body content for a database source.",
15
+ schema: z.object({
16
+ sourceId: z.string(),
17
+ documentId: z.string().optional(),
18
+ limit: z.number().int().positive().max(50).optional(),
19
+ }),
20
+ agentTool: false,
21
+ run: async (
22
+ args: ProcessBuilderBodyHydrationRequest,
23
+ ): Promise<ProcessBuilderBodyHydrationResponse> => {
24
+ const db = getDb();
25
+ const [source] = await db
26
+ .select({
27
+ id: schema.contentDatabaseSources.id,
28
+ databaseDocumentId: schema.contentDatabases.documentId,
29
+ })
30
+ .from(schema.contentDatabaseSources)
31
+ .innerJoin(
32
+ schema.contentDatabases,
33
+ eq(
34
+ schema.contentDatabases.id,
35
+ schema.contentDatabaseSources.databaseId,
36
+ ),
37
+ )
38
+ .where(eq(schema.contentDatabaseSources.id, args.sourceId));
39
+ if (!source) throw new Error(`Source "${args.sourceId}" not found`);
40
+ await assertAccess("document", source.databaseDocumentId, "editor");
41
+ return processBuilderBodyHydrationQueue({
42
+ sourceId: args.sourceId,
43
+ documentId: args.documentId,
44
+ limit: args.limit,
45
+ });
46
+ },
47
+ });