@agent-native/core 0.102.1 → 0.103.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 (295) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/docs/content/local-file-mode.mdx +99 -489
  4. package/corpus/core/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  5. package/corpus/core/docs/content/locales/ar-SA/template-content.mdx +10 -24
  6. package/corpus/core/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  7. package/corpus/core/docs/content/locales/de-DE/template-content.mdx +10 -24
  8. package/corpus/core/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  9. package/corpus/core/docs/content/locales/es-ES/template-content.mdx +10 -24
  10. package/corpus/core/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  11. package/corpus/core/docs/content/locales/fr-FR/template-content.mdx +10 -24
  12. package/corpus/core/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  13. package/corpus/core/docs/content/locales/hi-IN/template-content.mdx +10 -24
  14. package/corpus/core/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  15. package/corpus/core/docs/content/locales/ja-JP/template-content.mdx +10 -24
  16. package/corpus/core/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  17. package/corpus/core/docs/content/locales/ko-KR/template-content.mdx +10 -24
  18. package/corpus/core/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  19. package/corpus/core/docs/content/locales/pt-BR/template-content.mdx +10 -24
  20. package/corpus/core/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  21. package/corpus/core/docs/content/locales/zh-CN/template-content.mdx +9 -24
  22. package/corpus/core/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  23. package/corpus/core/docs/content/locales/zh-TW/template-content.mdx +9 -24
  24. package/corpus/core/docs/content/template-content.mdx +26 -42
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/cli/content-local.ts +86 -18
  27. package/corpus/core/src/cli/index.ts +2 -2
  28. package/corpus/core/src/cli/skills-content/content-skill.ts +36 -25
  29. package/corpus/core/src/cli/skills.ts +47 -13
  30. package/corpus/core/src/client/AgentPanel.tsx +7 -0
  31. package/corpus/core/src/client/AssistantChat.tsx +7 -0
  32. package/corpus/core/src/client/agent-chat-adapter.ts +7 -1
  33. package/corpus/core/src/client/chat/message-components.tsx +17 -3
  34. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
  35. package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
  36. package/corpus/core/src/client/error-format.ts +24 -0
  37. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
  38. package/corpus/core/src/local-artifacts/index.ts +65 -0
  39. package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
  40. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
  41. package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
  42. package/corpus/templates/assets/app/i18n-data.ts +215 -0
  43. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
  44. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
  45. package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -0
  46. package/corpus/templates/assets/app/routes/library.tsx +69 -0
  47. package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
  48. package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
  49. package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
  50. package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
  51. package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
  52. package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
  53. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +34 -13
  54. package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
  55. package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
  56. package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
  57. package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
  58. package/corpus/templates/clips/app/components/player/settings-panel.tsx +116 -58
  59. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
  60. package/corpus/templates/clips/app/components/player/video-player.tsx +50 -2
  61. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
  62. package/corpus/templates/clips/app/i18n/ar-SA.ts +5 -2
  63. package/corpus/templates/clips/app/i18n/de-DE.ts +6 -2
  64. package/corpus/templates/clips/app/i18n/en-US.ts +5 -2
  65. package/corpus/templates/clips/app/i18n/es-ES.ts +6 -2
  66. package/corpus/templates/clips/app/i18n/fr-FR.ts +5 -2
  67. package/corpus/templates/clips/app/i18n/hi-IN.ts +5 -2
  68. package/corpus/templates/clips/app/i18n/ja-JP.ts +6 -1
  69. package/corpus/templates/clips/app/i18n/ko-KR.ts +5 -2
  70. package/corpus/templates/clips/app/i18n/pt-BR.ts +6 -2
  71. package/corpus/templates/clips/app/i18n/zh-CN.ts +5 -2
  72. package/corpus/templates/clips/app/i18n/zh-TW.ts +5 -2
  73. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
  74. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
  75. package/corpus/templates/clips/app/routes/_index.tsx +1 -2
  76. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +98 -32
  77. package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
  78. package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
  80. package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
  81. package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
  82. package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
  88. package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
  89. package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
  90. package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
  91. package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
  92. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
  93. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
  94. package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
  95. package/corpus/templates/clips/server/lib/share-password.ts +6 -3
  96. package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
  97. package/corpus/templates/content/AGENTS.md +25 -30
  98. package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
  99. package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
  100. package/corpus/templates/content/actions/_content-files.ts +338 -0
  101. package/corpus/templates/content/actions/_content-space-access.ts +135 -0
  102. package/corpus/templates/content/actions/_content-spaces.ts +640 -0
  103. package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
  104. package/corpus/templates/content/actions/_database-utils.ts +7 -1
  105. package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
  106. package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
  107. package/corpus/templates/content/actions/_property-utils.ts +9 -4
  108. package/corpus/templates/content/actions/add-database-item.ts +108 -65
  109. package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
  110. package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
  111. package/corpus/templates/content/actions/create-content-database.ts +129 -3
  112. package/corpus/templates/content/actions/create-document.ts +65 -55
  113. package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
  114. package/corpus/templates/content/actions/delete-content-database.ts +3 -0
  115. package/corpus/templates/content/actions/delete-document.ts +7 -11
  116. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
  117. package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
  118. package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
  119. package/corpus/templates/content/actions/edit-document.ts +2 -28
  120. package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
  121. package/corpus/templates/content/actions/export-content-source.ts +47 -9
  122. package/corpus/templates/content/actions/get-document.ts +0 -14
  123. package/corpus/templates/content/actions/import-content-source.ts +43 -3
  124. package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
  125. package/corpus/templates/content/actions/list-documents.ts +12 -9
  126. package/corpus/templates/content/actions/move-document.ts +3 -14
  127. package/corpus/templates/content/actions/pull-document.ts +0 -23
  128. package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
  129. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
  130. package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
  131. package/corpus/templates/content/actions/search-documents.ts +0 -34
  132. package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
  133. package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
  134. package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
  135. package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
  136. package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
  137. package/corpus/templates/content/actions/update-document.ts +0 -15
  138. package/corpus/templates/content/actions/view-screen.ts +2 -50
  139. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
  140. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
  141. package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
  142. package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
  143. package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
  144. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
  145. package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
  146. package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
  147. package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
  148. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
  149. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
  150. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
  151. package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
  152. package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
  153. package/corpus/templates/content/app/i18n-data.ts +75 -0
  154. package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
  155. package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
  156. package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
  157. package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
  158. package/corpus/templates/content/parity/matrix.md +2 -1
  159. package/corpus/templates/content/parity/matrix.ts +37 -1
  160. package/corpus/templates/content/scripts/dev-database.mjs +7 -5
  161. package/corpus/templates/content/server/db/schema.ts +113 -40
  162. package/corpus/templates/content/server/lib/document-context.ts +15 -1
  163. package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
  164. package/corpus/templates/content/server/plugins/db.ts +153 -0
  165. package/corpus/templates/content/shared/api.ts +17 -3
  166. package/corpus/templates/content/shared/builder-mdx.ts +163 -16
  167. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
  168. package/corpus/templates/design/AGENTS.md +6 -1
  169. package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
  170. package/corpus/templates/design/actions/list-design-templates.ts +69 -21
  171. package/corpus/templates/design/actions/navigate.ts +2 -2
  172. package/corpus/templates/design/actions/view-screen.ts +15 -2
  173. package/corpus/templates/design/agent-native.app-skill.json +5 -0
  174. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
  175. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
  176. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
  177. package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
  178. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
  179. package/corpus/templates/design/app/i18n-data.ts +21 -2
  180. package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
  181. package/corpus/templates/design/app/pages/Index.tsx +195 -24
  182. package/corpus/templates/design/app/pages/Templates.tsx +144 -55
  183. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
  184. package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
  185. package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
  186. package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
  187. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  188. package/corpus/templates/mail/AGENTS.md +23 -18
  189. package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
  190. package/corpus/templates/mail/actions/mark-read.ts +131 -20
  191. package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
  192. package/corpus/templates/mail/actions/move-email.ts +30 -24
  193. package/corpus/templates/mail/actions/send-email.ts +73 -68
  194. package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
  195. package/corpus/templates/mail/server/handlers/emails.ts +218 -209
  196. package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
  197. package/corpus/templates/mail/server/lib/email-state.ts +198 -113
  198. package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
  199. package/corpus/templates/mail/server/lib/jobs.ts +69 -74
  200. package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
  201. package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
  202. package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
  203. package/corpus/templates/slides/actions/import-file.ts +4 -1
  204. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
  205. package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
  206. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
  207. package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
  208. package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
  209. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  210. package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
  211. package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
  212. package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
  213. package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
  214. package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
  215. package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
  216. package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
  217. package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
  218. package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
  219. package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
  220. package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
  221. package/dist/cli/content-local.d.ts +1 -0
  222. package/dist/cli/content-local.d.ts.map +1 -1
  223. package/dist/cli/content-local.js +75 -17
  224. package/dist/cli/content-local.js.map +1 -1
  225. package/dist/cli/index.js +2 -2
  226. package/dist/cli/index.js.map +1 -1
  227. package/dist/cli/skills-content/content-skill.d.ts +1 -1
  228. package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
  229. package/dist/cli/skills-content/content-skill.js +36 -25
  230. package/dist/cli/skills-content/content-skill.js.map +1 -1
  231. package/dist/cli/skills.d.ts.map +1 -1
  232. package/dist/cli/skills.js +47 -14
  233. package/dist/cli/skills.js.map +1 -1
  234. package/dist/client/AgentPanel.d.ts.map +1 -1
  235. package/dist/client/AgentPanel.js +5 -0
  236. package/dist/client/AgentPanel.js.map +1 -1
  237. package/dist/client/AssistantChat.d.ts.map +1 -1
  238. package/dist/client/AssistantChat.js +7 -0
  239. package/dist/client/AssistantChat.js.map +1 -1
  240. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  241. package/dist/client/agent-chat-adapter.js +8 -1
  242. package/dist/client/agent-chat-adapter.js.map +1 -1
  243. package/dist/client/chat/message-components.d.ts +4 -0
  244. package/dist/client/chat/message-components.d.ts.map +1 -1
  245. package/dist/client/chat/message-components.js +10 -3
  246. package/dist/client/chat/message-components.js.map +1 -1
  247. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  248. package/dist/client/composer/ComposerPlusMenu.js +58 -6
  249. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  250. package/dist/client/composer/asset-picker-url.d.ts +16 -0
  251. package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
  252. package/dist/client/composer/asset-picker-url.js +64 -0
  253. package/dist/client/composer/asset-picker-url.js.map +1 -0
  254. package/dist/client/error-format.d.ts.map +1 -1
  255. package/dist/client/error-format.js +17 -0
  256. package/dist/client/error-format.js.map +1 -1
  257. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  258. package/dist/client/resources/McpIntegrationDialog.js +6 -1
  259. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  260. package/dist/collab/awareness.d.ts +2 -2
  261. package/dist/collab/awareness.d.ts.map +1 -1
  262. package/dist/collab/struct-routes.d.ts +1 -1
  263. package/dist/local-artifacts/index.d.ts +10 -0
  264. package/dist/local-artifacts/index.d.ts.map +1 -1
  265. package/dist/local-artifacts/index.js +39 -0
  266. package/dist/local-artifacts/index.js.map +1 -1
  267. package/dist/notifications/routes.d.ts +1 -1
  268. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  269. package/dist/resources/handlers.d.ts +3 -3
  270. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  271. package/dist/server/transcribe-voice.d.ts +1 -1
  272. package/docs/content/local-file-mode.mdx +99 -489
  273. package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  274. package/docs/content/locales/ar-SA/template-content.mdx +10 -24
  275. package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  276. package/docs/content/locales/de-DE/template-content.mdx +10 -24
  277. package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  278. package/docs/content/locales/es-ES/template-content.mdx +10 -24
  279. package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  280. package/docs/content/locales/fr-FR/template-content.mdx +10 -24
  281. package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  282. package/docs/content/locales/hi-IN/template-content.mdx +10 -24
  283. package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  284. package/docs/content/locales/ja-JP/template-content.mdx +10 -24
  285. package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  286. package/docs/content/locales/ko-KR/template-content.mdx +10 -24
  287. package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  288. package/docs/content/locales/pt-BR/template-content.mdx +10 -24
  289. package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  290. package/docs/content/locales/zh-CN/template-content.mdx +9 -24
  291. package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  292. package/docs/content/locales/zh-TW/template-content.mdx +9 -24
  293. package/docs/content/template-content.mdx +26 -42
  294. package/package.json +1 -1
  295. package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
@@ -0,0 +1,731 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { defineAction } from "@agent-native/core";
4
+ import { writeAppState } from "@agent-native/core/application-state";
5
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
6
+ import { assertAccess } from "@agent-native/core/sharing";
7
+ import { and, eq, inArray } from "drizzle-orm";
8
+ import { z } from "zod";
9
+
10
+ import { getDb, schema } from "../server/db/index.js";
11
+ import type { ContentDatabaseSourceTruthPolicy } from "../shared/api.js";
12
+ import {
13
+ isBuilderMdxSourcePath,
14
+ isContentSourcePath,
15
+ parseContentSourceFile,
16
+ } from "../shared/content-source.js";
17
+ import { ensureDocumentsFilesMembership } from "./_content-files.js";
18
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
19
+ import { LOCAL_FOLDER_SOURCE_TYPE } from "./_local-folder-source.js";
20
+
21
+ const MAX_SOURCE_FILES = 500;
22
+ const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024;
23
+
24
+ function opaqueId(kind: string, value: string) {
25
+ return `${kind}_${createHash("sha256").update(value).digest("hex").slice(0, 32)}`;
26
+ }
27
+
28
+ function contentHash(content: string) {
29
+ return createHash("sha256").update(content).digest("hex");
30
+ }
31
+
32
+ function metadataHash(value: {
33
+ title: string;
34
+ description?: string | null;
35
+ icon?: string | null;
36
+ }) {
37
+ return contentHash(
38
+ JSON.stringify({
39
+ title: value.title,
40
+ description: value.description ?? "",
41
+ icon: value.icon ?? null,
42
+ }),
43
+ );
44
+ }
45
+
46
+ function parseJson(value: string | null | undefined) {
47
+ if (!value) return {} as Record<string, unknown>;
48
+ try {
49
+ const parsed = JSON.parse(value) as unknown;
50
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
51
+ ? (parsed as Record<string, unknown>)
52
+ : {};
53
+ } catch {
54
+ return {};
55
+ }
56
+ }
57
+
58
+ function truthPolicy(value: unknown): ContentDatabaseSourceTruthPolicy {
59
+ return value === "source_primary" || value === "reviewed_bidirectional"
60
+ ? value
61
+ : "database_primary";
62
+ }
63
+
64
+ function normalizedEntries(files: Record<string, string>) {
65
+ return Object.entries(files)
66
+ .filter(([path]) => isContentSourcePath(path))
67
+ .sort(([left], [right]) => left.localeCompare(right));
68
+ }
69
+
70
+ function sourceValues(args: {
71
+ path: string;
72
+ title: string;
73
+ hash: string;
74
+ metadataHash: string;
75
+ }) {
76
+ return JSON.stringify({
77
+ relativePath: args.path,
78
+ extension: args.path.toLowerCase().endsWith(".mdx") ? ".mdx" : ".md",
79
+ title: args.title,
80
+ contentHash: args.hash,
81
+ metadataHash: args.metadataHash,
82
+ });
83
+ }
84
+
85
+ function bodyChangeJson(args: {
86
+ currentHash: string;
87
+ incomingHash: string;
88
+ currentMetadataHash: string;
89
+ incomingMetadataHash: string;
90
+ }) {
91
+ return JSON.stringify({
92
+ summary: "Local folder and Content both changed since the last sync.",
93
+ currentExcerpt: null,
94
+ proposedExcerpt: null,
95
+ currentHash: args.currentHash,
96
+ proposedHash: args.incomingHash,
97
+ currentMetadataHash: args.currentMetadataHash,
98
+ proposedMetadataHash: args.incomingMetadataHash,
99
+ });
100
+ }
101
+
102
+ function deletionChangeJson(args: {
103
+ path: string;
104
+ previousHash: string | null;
105
+ }) {
106
+ return JSON.stringify({
107
+ operation: "source_delete",
108
+ relativePath: args.path,
109
+ previousHash: args.previousHash,
110
+ });
111
+ }
112
+
113
+ function metadataChanges(
114
+ current: { title: string; description?: string | null; icon?: string | null },
115
+ proposed: {
116
+ title: string;
117
+ description?: string | null;
118
+ icon?: string | null;
119
+ },
120
+ ) {
121
+ return (["title", "description", "icon"] as const).flatMap((field) => {
122
+ const currentValue =
123
+ current[field] ?? (field === "description" ? "" : null);
124
+ const proposedValue =
125
+ proposed[field] ?? (field === "description" ? "" : null);
126
+ return currentValue === proposedValue
127
+ ? []
128
+ : [{ field, currentValue, proposedValue }];
129
+ });
130
+ }
131
+
132
+ export default defineAction({
133
+ description:
134
+ "Refresh a connected local-folder source through the trusted browser/Desktop bridge, materializing Markdown/MDX as normal SQL-backed Content pages and recording safe source identity and conflicts.",
135
+ schema: z.object({
136
+ sourceId: z.string().min(1),
137
+ files: z
138
+ .record(z.string(), z.string().max(MAX_SOURCE_FILE_BYTES))
139
+ .refine((value) => Object.keys(value).length <= MAX_SOURCE_FILES, {
140
+ message: `Sync is limited to ${MAX_SOURCE_FILES} files.`,
141
+ }),
142
+ dryRun: z.boolean().optional().default(false),
143
+ }),
144
+ run: async ({ sourceId, files, dryRun }) => {
145
+ const userEmail = getRequestUserEmail();
146
+ if (!userEmail) throw new Error("no authenticated user");
147
+ const builderPaths = Object.keys(files).filter(isBuilderMdxSourcePath);
148
+ if (builderPaths.length) {
149
+ throw new Error(
150
+ `Builder .builder.mdx files require the Builder actions: ${builderPaths.join(", ")}`,
151
+ );
152
+ }
153
+ const entries = normalizedEntries(files);
154
+ const db = getDb();
155
+ const [target] = await db
156
+ .select({
157
+ source: schema.contentDatabaseSources,
158
+ database: schema.contentDatabases,
159
+ })
160
+ .from(schema.contentDatabaseSources)
161
+ .innerJoin(
162
+ schema.contentDatabases,
163
+ eq(
164
+ schema.contentDatabases.id,
165
+ schema.contentDatabaseSources.databaseId,
166
+ ),
167
+ )
168
+ .where(eq(schema.contentDatabaseSources.id, sourceId));
169
+ if (
170
+ !target ||
171
+ target.source.sourceType !== LOCAL_FOLDER_SOURCE_TYPE ||
172
+ target.database.systemRole !== "files" ||
173
+ !target.database.spaceId
174
+ ) {
175
+ throw new Error(`Local folder source "${sourceId}" not found`);
176
+ }
177
+ await resolveContentSpaceAccess(target.database.spaceId, "editor");
178
+ const targetSpaceId = target.database.spaceId;
179
+
180
+ const parsed = entries.map(([path, value]) =>
181
+ parseContentSourceFile(path, value),
182
+ );
183
+ const parseErrors = parsed.flatMap((file) =>
184
+ file.errors?.length
185
+ ? [{ path: file.path, reason: file.errors.join(" ") }]
186
+ : [],
187
+ );
188
+ const valid = parsed.filter((file) => !file.errors?.length);
189
+ const duplicateIds = new Set<string>();
190
+ const seenIds = new Set<string>();
191
+ for (const file of valid) {
192
+ if (!file.id) continue;
193
+ if (seenIds.has(file.id)) duplicateIds.add(file.id);
194
+ seenIds.add(file.id);
195
+ }
196
+ if (duplicateIds.size) {
197
+ throw new Error(
198
+ `Duplicate source ids: ${[...duplicateIds].sort().join(", ")}`,
199
+ );
200
+ }
201
+
202
+ const explicitIds = valid.flatMap((file) => (file.id ? [file.id] : []));
203
+ type SourceRow = typeof schema.contentDatabaseSourceRows.$inferSelect;
204
+ type Document = typeof schema.documents.$inferSelect;
205
+ const loadSnapshot = async (queryDb: any) => {
206
+ const storedRows = (await queryDb
207
+ .select()
208
+ .from(schema.contentDatabaseSourceRows)
209
+ .where(
210
+ eq(schema.contentDatabaseSourceRows.sourceId, sourceId),
211
+ )) as SourceRow[];
212
+ const rowByPath = new Map(
213
+ storedRows.map((row) => [
214
+ String(parseJson(row.sourceValuesJson).relativePath ?? ""),
215
+ row,
216
+ ]),
217
+ );
218
+ const rowByDocumentId = new Map(
219
+ storedRows.map((row) => [row.documentId, row]),
220
+ );
221
+ const linkedIds = storedRows.map((row) => row.documentId);
222
+ const candidateIds = [...new Set([...explicitIds, ...linkedIds])];
223
+ const existingDocuments = (
224
+ candidateIds.length
225
+ ? await queryDb
226
+ .select()
227
+ .from(schema.documents)
228
+ .where(inArray(schema.documents.id, candidateIds))
229
+ : []
230
+ ) as Document[];
231
+ const documentById = new Map(
232
+ existingDocuments.map((document) => [document.id, document]),
233
+ );
234
+ return { storedRows, rowByPath, rowByDocumentId, documentById };
235
+ };
236
+
237
+ const initialSnapshot = await loadSnapshot(db);
238
+
239
+ const validateDocumentIds = async (
240
+ snapshot: Awaited<ReturnType<typeof loadSnapshot>>,
241
+ ) => {
242
+ const documentIds = new Set([
243
+ ...explicitIds,
244
+ ...snapshot.storedRows.map((row) => row.documentId),
245
+ ]);
246
+ for (const id of documentIds) {
247
+ const existing = snapshot.documentById.get(id);
248
+ if (!existing) continue;
249
+ if (existing.spaceId !== targetSpaceId) {
250
+ throw new Error(
251
+ `Document "${id}" belongs to another Content space and cannot be imported here`,
252
+ );
253
+ }
254
+ await assertAccess("document", id, "editor");
255
+ }
256
+ };
257
+
258
+ await validateDocumentIds(initialSnapshot);
259
+
260
+ const metadata = parseJson(target.source.metadataJson);
261
+ const policy = truthPolicy(metadata.truthPolicy);
262
+ const now = new Date().toISOString();
263
+ const created: Array<{ id: string; path: string; title: string }> = [];
264
+ const updated: Array<{ id: string; path: string; title: string }> = [];
265
+ const unchanged: Array<{ id: string; path: string; title: string }> = [];
266
+ const skipped = [...parseErrors];
267
+ const conflicts: Array<{ id: string; path: string; title: string }> = [];
268
+ const outbound: Array<{ id: string; path: string; title: string }> = [];
269
+
270
+ const buildPlans = (snapshot: Awaited<ReturnType<typeof loadSnapshot>>) =>
271
+ valid.map((file, index) => {
272
+ const pathRow = snapshot.rowByPath.get(file.path);
273
+ const id =
274
+ file.id ??
275
+ pathRow?.documentId ??
276
+ opaqueId("content_local_file", `${sourceId}:${file.path}`);
277
+ const existing = snapshot.documentById.get(id);
278
+ const previousRow = snapshot.rowByDocumentId.get(id) ?? pathRow;
279
+ const previousValues = parseJson(previousRow?.sourceValuesJson);
280
+ const previousHash =
281
+ typeof previousValues.contentHash === "string"
282
+ ? previousValues.contentHash
283
+ : null;
284
+ const previousMetadataHash =
285
+ typeof previousValues.metadataHash === "string"
286
+ ? previousValues.metadataHash
287
+ : null;
288
+ const incomingHash = contentHash(file.content);
289
+ const incomingMetadataHash = metadataHash(file);
290
+ const localHash = existing ? contentHash(existing.content) : null;
291
+ const localMetadataHash = existing ? metadataHash(existing) : null;
292
+ const incomingChanged =
293
+ previousHash !== incomingHash ||
294
+ previousMetadataHash !== incomingMetadataHash;
295
+ const localChanged =
296
+ !!existing &&
297
+ ((!!previousHash && localHash !== previousHash) ||
298
+ (!!previousMetadataHash &&
299
+ localMetadataHash !== previousMetadataHash));
300
+ const conflict =
301
+ !!existing &&
302
+ !!previousHash &&
303
+ incomingChanged &&
304
+ (localChanged || policy === "database_primary") &&
305
+ (localHash !== incomingHash ||
306
+ localMetadataHash !== incomingMetadataHash);
307
+ const keepContent =
308
+ !!existing &&
309
+ !!previousHash &&
310
+ localChanged &&
311
+ !incomingChanged &&
312
+ policy !== "source_primary";
313
+ const applyIncoming =
314
+ incomingChanged || (localChanged && policy === "source_primary");
315
+ return {
316
+ file,
317
+ index,
318
+ id,
319
+ existing,
320
+ previousRow,
321
+ previousHash,
322
+ incomingHash,
323
+ incomingMetadataHash,
324
+ localHash,
325
+ localMetadataHash,
326
+ incomingChanged,
327
+ localChanged,
328
+ conflict,
329
+ keepContent,
330
+ applyIncoming,
331
+ };
332
+ });
333
+
334
+ let plans = buildPlans(initialSnapshot);
335
+ const initialDocumentIds = new Set(plans.map((plan) => plan.id));
336
+ let missingRows = initialSnapshot.storedRows.filter(
337
+ (row) => !initialDocumentIds.has(row.documentId),
338
+ );
339
+ const classifyPlans = (
340
+ snapshot: Awaited<ReturnType<typeof loadSnapshot>>,
341
+ ) => {
342
+ created.length = 0;
343
+ updated.length = 0;
344
+ unchanged.length = 0;
345
+ conflicts.length = 0;
346
+ outbound.length = 0;
347
+ for (const row of missingRows) {
348
+ const values = parseJson(row.sourceValuesJson);
349
+ const path = String(values.relativePath ?? row.sourceDisplayKey ?? "");
350
+ const document = snapshot.documentById.get(row.documentId);
351
+ conflicts.push({
352
+ id: row.documentId,
353
+ path,
354
+ title: document?.title ?? path,
355
+ });
356
+ }
357
+
358
+ for (const plan of plans) {
359
+ if (plan.conflict) {
360
+ conflicts.push({
361
+ id: plan.id,
362
+ path: plan.file.path,
363
+ title: plan.file.title,
364
+ });
365
+ } else if (!plan.existing) {
366
+ created.push({
367
+ id: plan.id,
368
+ path: plan.file.path,
369
+ title: plan.file.title,
370
+ });
371
+ } else if (
372
+ plan.applyIncoming ||
373
+ plan.existing.title !== plan.file.title ||
374
+ plan.existing.sourcePath !== plan.file.path
375
+ ) {
376
+ updated.push({
377
+ id: plan.id,
378
+ path: plan.file.path,
379
+ title: plan.file.title,
380
+ });
381
+ } else {
382
+ unchanged.push({
383
+ id: plan.id,
384
+ path: plan.file.path,
385
+ title: plan.file.title,
386
+ });
387
+ }
388
+ if (plan.keepContent) {
389
+ outbound.push({
390
+ id: plan.id,
391
+ path: plan.file.path,
392
+ title: plan.file.title,
393
+ });
394
+ }
395
+ }
396
+ };
397
+ classifyPlans(initialSnapshot);
398
+
399
+ if (!dryRun) {
400
+ await db.transaction(async (tx: any) => {
401
+ const claimedSources = await tx
402
+ .update(schema.contentDatabaseSources)
403
+ .set({ updatedAt: now })
404
+ .where(
405
+ and(
406
+ eq(schema.contentDatabaseSources.id, sourceId),
407
+ eq(schema.contentDatabaseSources.databaseId, target.database.id),
408
+ eq(
409
+ schema.contentDatabaseSources.sourceType,
410
+ LOCAL_FOLDER_SOURCE_TYPE,
411
+ ),
412
+ ),
413
+ )
414
+ .returning({ id: schema.contentDatabaseSources.id });
415
+ if (claimedSources.length !== 1) {
416
+ throw new Error(
417
+ `Local folder source "${sourceId}" was disconnected before sync`,
418
+ );
419
+ }
420
+ const transactionSnapshot = await loadSnapshot(tx);
421
+ await validateDocumentIds(transactionSnapshot);
422
+ plans = buildPlans(transactionSnapshot);
423
+ const currentDocumentIds = new Set(plans.map((plan) => plan.id));
424
+ missingRows = transactionSnapshot.storedRows.filter(
425
+ (row) => !currentDocumentIds.has(row.documentId),
426
+ );
427
+ classifyPlans(transactionSnapshot);
428
+ for (const plan of plans) {
429
+ if (plan.conflict) {
430
+ const changeSetId = opaqueId(
431
+ "content_source_change",
432
+ `${sourceId}:${plan.id}:${plan.incomingHash}:${plan.incomingMetadataHash}:${plan.localHash}:${plan.localMetadataHash}`,
433
+ );
434
+ await tx
435
+ .insert(schema.contentDatabaseSourceChangeSets)
436
+ .values({
437
+ id: changeSetId,
438
+ ownerEmail: target.source.ownerEmail,
439
+ sourceId,
440
+ databaseItemId: plan.previousRow?.databaseItemId ?? null,
441
+ documentId: plan.id,
442
+ kind:
443
+ plan.localHash === plan.incomingHash
444
+ ? "metadata_update"
445
+ : "body_update",
446
+ direction: "incoming",
447
+ state: "proposed",
448
+ pushMode: "none",
449
+ localOnly: 0,
450
+ summary: `Review concurrent local-folder changes for "${plan.file.title}".`,
451
+ fieldChangesJson: JSON.stringify(
452
+ metadataChanges(plan.existing!, plan.file),
453
+ ),
454
+ bodyChangeJson: bodyChangeJson({
455
+ currentHash: plan.localHash!,
456
+ incomingHash: plan.incomingHash,
457
+ currentMetadataHash: plan.localMetadataHash!,
458
+ incomingMetadataHash: plan.incomingMetadataHash,
459
+ }),
460
+ createdAt: now,
461
+ updatedAt: now,
462
+ })
463
+ .onConflictDoNothing();
464
+ continue;
465
+ }
466
+
467
+ if (plan.keepContent) {
468
+ const changeSetId = opaqueId(
469
+ "content_source_change",
470
+ `${sourceId}:${plan.id}:outbound:${plan.localHash}`,
471
+ );
472
+ await tx
473
+ .insert(schema.contentDatabaseSourceChangeSets)
474
+ .values({
475
+ id: changeSetId,
476
+ ownerEmail: target.source.ownerEmail,
477
+ sourceId,
478
+ databaseItemId: plan.previousRow?.databaseItemId ?? null,
479
+ documentId: plan.id,
480
+ kind: "body_update",
481
+ direction: "outbound",
482
+ state: "proposed",
483
+ pushMode: "none",
484
+ localOnly: 1,
485
+ summary: `Content has a newer revision of "${plan.file.title}" ready to export.`,
486
+ fieldChangesJson: "[]",
487
+ bodyChangeJson: bodyChangeJson({
488
+ currentHash: plan.incomingHash,
489
+ incomingHash: plan.localHash!,
490
+ currentMetadataHash: plan.incomingMetadataHash,
491
+ incomingMetadataHash: plan.localMetadataHash!,
492
+ }),
493
+ createdAt: now,
494
+ updatedAt: now,
495
+ })
496
+ .onConflictDoNothing();
497
+ continue;
498
+ }
499
+
500
+ if (!plan.existing) {
501
+ await tx.insert(schema.documents).values({
502
+ id: plan.id,
503
+ spaceId: target.database.spaceId,
504
+ ownerEmail: userEmail,
505
+ orgId: target.database.orgId,
506
+ parentId: null,
507
+ title: plan.file.title,
508
+ content: plan.file.content,
509
+ description: plan.file.description ?? "",
510
+ icon: plan.file.icon ?? null,
511
+ position: plan.file.position ?? plan.index,
512
+ isFavorite: plan.file.isFavorite ? 1 : 0,
513
+ hideFromSearch: plan.file.hideFromSearch ? 1 : 0,
514
+ sourceMode: "local-files",
515
+ sourceKind: "file",
516
+ sourcePath: plan.file.path,
517
+ sourceRootPath: target.source.sourceName,
518
+ sourceUpdatedAt: now,
519
+ visibility: target.database.orgId ? "org" : "private",
520
+ createdAt: now,
521
+ updatedAt: now,
522
+ });
523
+ } else if (
524
+ plan.applyIncoming ||
525
+ plan.existing.sourcePath !== plan.file.path
526
+ ) {
527
+ await tx
528
+ .insert(schema.documentVersions)
529
+ .values({
530
+ id: opaqueId(
531
+ "content_document_version",
532
+ `${plan.id}:${plan.existing.updatedAt}:${plan.incomingHash}`,
533
+ ),
534
+ ownerEmail: plan.existing.ownerEmail,
535
+ documentId: plan.id,
536
+ title: plan.existing.title,
537
+ content: plan.existing.content,
538
+ createdAt: now,
539
+ })
540
+ .onConflictDoNothing();
541
+ const reboundDocuments = await tx
542
+ .update(schema.documents)
543
+ .set({
544
+ ...(plan.applyIncoming
545
+ ? {
546
+ title: plan.file.title,
547
+ content: plan.file.content,
548
+ description:
549
+ plan.file.description ?? plan.existing.description,
550
+ icon: plan.file.icon ?? plan.existing.icon,
551
+ }
552
+ : {}),
553
+ sourceMode: "local-files",
554
+ sourceKind: "file",
555
+ sourcePath: plan.file.path,
556
+ sourceRootPath: target.source.sourceName,
557
+ sourceUpdatedAt: now,
558
+ updatedAt: now,
559
+ })
560
+ .where(
561
+ and(
562
+ eq(schema.documents.id, plan.id),
563
+ eq(schema.documents.spaceId, targetSpaceId),
564
+ ),
565
+ )
566
+ .returning({ id: schema.documents.id });
567
+ if (reboundDocuments.length !== 1) {
568
+ throw new Error(
569
+ `Document "${plan.id}" left this Content space during local-folder sync`,
570
+ );
571
+ }
572
+ }
573
+ }
574
+
575
+ const materializedIds = plans
576
+ .filter((plan) => !plan.conflict)
577
+ .map((plan) => plan.id);
578
+ await ensureDocumentsFilesMembership(tx, materializedIds, now);
579
+ const filesItems = materializedIds.length
580
+ ? await tx
581
+ .select()
582
+ .from(schema.contentDatabaseItems)
583
+ .where(
584
+ and(
585
+ eq(
586
+ schema.contentDatabaseItems.databaseId,
587
+ target.database.id,
588
+ ),
589
+ inArray(
590
+ schema.contentDatabaseItems.documentId,
591
+ materializedIds,
592
+ ),
593
+ ),
594
+ )
595
+ : [];
596
+ const itemByDocumentId = new Map<
597
+ string,
598
+ typeof schema.contentDatabaseItems.$inferSelect
599
+ >(
600
+ filesItems.map(
601
+ (item: typeof schema.contentDatabaseItems.$inferSelect) =>
602
+ [item.documentId, item] as const,
603
+ ),
604
+ );
605
+ for (const plan of plans.filter((candidate) => !candidate.conflict)) {
606
+ if (plan.keepContent) continue;
607
+ const item = itemByDocumentId.get(plan.id);
608
+ if (!item) {
609
+ throw new Error(
610
+ `Files membership was not created for "${plan.id}"`,
611
+ );
612
+ }
613
+ const rowId = opaqueId(
614
+ "content_source_row",
615
+ `${sourceId}:${plan.id}`,
616
+ );
617
+ const rowValues = {
618
+ sourceId,
619
+ databaseItemId: item.id,
620
+ documentId: plan.id,
621
+ sourceRowId: plan.file.path,
622
+ sourceQualifiedId: `local-folder://${target.source.sourceTable}/${encodeURIComponent(plan.file.path)}`,
623
+ sourceDisplayKey: plan.file.path,
624
+ sourceValuesJson: sourceValues({
625
+ path: plan.file.path,
626
+ title: plan.file.title,
627
+ hash: plan.incomingHash,
628
+ metadataHash: plan.incomingMetadataHash,
629
+ }),
630
+ provenance: "trusted local-folder bridge",
631
+ syncState: "linked",
632
+ freshness: "fresh",
633
+ lastSyncedAt: now,
634
+ lastSourceUpdatedAt: now,
635
+ updatedAt: now,
636
+ };
637
+ await tx
638
+ .insert(schema.contentDatabaseSourceRows)
639
+ .values({
640
+ id: rowId,
641
+ ownerEmail: target.source.ownerEmail,
642
+ createdAt: now,
643
+ ...rowValues,
644
+ })
645
+ .onConflictDoNothing();
646
+ await tx
647
+ .update(schema.contentDatabaseSourceRows)
648
+ .set(rowValues)
649
+ .where(eq(schema.contentDatabaseSourceRows.id, rowId));
650
+ }
651
+ for (const row of missingRows) {
652
+ const values = parseJson(row.sourceValuesJson);
653
+ const path = String(
654
+ values.relativePath ?? row.sourceDisplayKey ?? row.sourceRowId,
655
+ );
656
+ const changeSetId = opaqueId(
657
+ "content_source_change",
658
+ `${sourceId}:${row.documentId}:source-delete:${path}`,
659
+ );
660
+ await tx
661
+ .insert(schema.contentDatabaseSourceChangeSets)
662
+ .values({
663
+ id: changeSetId,
664
+ ownerEmail: target.source.ownerEmail,
665
+ sourceId,
666
+ databaseItemId: row.databaseItemId,
667
+ documentId: row.documentId,
668
+ kind: "metadata_update",
669
+ direction: "incoming",
670
+ state: "proposed",
671
+ pushMode: "none",
672
+ localOnly: 0,
673
+ summary: `The local source no longer contains "${path}". Review before unlinking it.`,
674
+ fieldChangesJson: "[]",
675
+ bodyChangeJson: deletionChangeJson({
676
+ path,
677
+ previousHash:
678
+ typeof values.contentHash === "string"
679
+ ? values.contentHash
680
+ : null,
681
+ }),
682
+ createdAt: now,
683
+ updatedAt: now,
684
+ })
685
+ .onConflictDoNothing();
686
+ }
687
+ const refreshedSources = await tx
688
+ .update(schema.contentDatabaseSources)
689
+ .set({
690
+ syncState: conflicts.length ? "error" : "linked",
691
+ freshness: conflicts.length || outbound.length ? "stale" : "fresh",
692
+ lastRefreshedAt: now,
693
+ lastSourceUpdatedAt: now,
694
+ lastError: conflicts.length
695
+ ? `${conflicts.length} file conflict${conflicts.length === 1 ? "" : "s"} require review.`
696
+ : outbound.length
697
+ ? `${outbound.length} Content change${outbound.length === 1 ? " is" : "s are"} ready to export.`
698
+ : null,
699
+ updatedAt: now,
700
+ })
701
+ .where(eq(schema.contentDatabaseSources.id, sourceId))
702
+ .returning({ id: schema.contentDatabaseSources.id });
703
+ if (refreshedSources.length !== 1) {
704
+ throw new Error(
705
+ `Local folder source "${sourceId}" was disconnected during sync`,
706
+ );
707
+ }
708
+ });
709
+ await writeAppState("refresh-signal", { ts: Date.now() });
710
+ }
711
+
712
+ return {
713
+ sourceId,
714
+ spaceId: target.database.spaceId,
715
+ filesDatabaseId: target.database.id,
716
+ truthPolicy: policy,
717
+ dryRun,
718
+ filesSeen: entries.length,
719
+ created,
720
+ updated,
721
+ unchanged,
722
+ conflicts,
723
+ outbound,
724
+ skipped,
725
+ errors: parseErrors,
726
+ idByPath: Object.fromEntries(
727
+ plans.map((plan) => [plan.file.path, plan.id]),
728
+ ),
729
+ };
730
+ },
731
+ });