@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
@@ -3,7 +3,7 @@ name: content
3
3
  description: >-
4
4
  Use Content for repo-backed Markdown/MDX docs, blogs, resources, rich
5
5
  document editing, Notion-style databases and boards, structured intake
6
- forms, local components, shareable copies, and Content local-file workspaces.
6
+ forms, local components, shareable copies, and connected local folders.
7
7
  Prefer Content actions over raw filesystem writes when available.
8
8
  metadata:
9
9
  visibility: exported
@@ -17,7 +17,7 @@ marketing pages, internal notes, and local MDX components. Also use Content for
17
17
  Notion-style databases, tables, boards, structured request intake, and forms
18
18
  whose submissions become database row pages. Content gives the agent a document
19
19
  tree, a rich editor, structured database properties and views, normal document
20
- actions, and an optional local-file source of truth.
20
+ actions, and local folders that sync into the same database model.
21
21
 
22
22
  ## Choose The Path
23
23
 
@@ -33,9 +33,9 @@ actions, and an optional local-file source of truth.
33
33
  its exact title, then inspect it with `get-content-database` before creating
34
34
  or submitting anything. Do not create a second database when the canonical
35
35
  one already exists.
36
- - In Local File Mode, Content actions read and write the repo files declared in
37
- `agent-native.json`; SQL remains cache/history/search glue, not the source of
38
- truth for those pages.
36
+ - Local folders are sources attached to a space's canonical Files database.
37
+ Imported pages are normal SQL-backed Content documents; the trusted local
38
+ bridge handles pull, export, stable file identity, and conflict review.
39
39
  - If Content tools are not visible and no local Content app or Desktop bridge is
40
40
  running, treat this skill as repo-editing guidance. Edit configured
41
41
  `.md`/`.mdx` files directly, preserve frontmatter and MDX imports, and tell
@@ -47,10 +47,10 @@ Prefer JSON input for action calls:
47
47
 
48
48
  ```bash
49
49
  pnpm action list-documents
50
- pnpm action get-document '{"id":"local-file:..."}'
51
- pnpm action edit-document '{"id":"local-file:...","find":"old copy","replace":"new copy"}'
52
- pnpm action update-document '{"id":"local-file:...","content":"# Updated\n\nBody"}'
53
- pnpm action share-local-file-document '{"id":"local-file:..."}'
50
+ pnpm action get-document '{"id":"<document-id>"}'
51
+ pnpm action edit-document '{"id":"<document-id>","find":"old copy","replace":"new copy"}'
52
+ pnpm action update-document '{"id":"<document-id>","content":"# Updated\n\nBody"}'
53
+ pnpm action connect-local-folder-source '{"connectionId":"<opaque-bridge-id>","label":"Docs","createSourceBackedSpace":true,"truthPolicy":"source_primary"}'
54
54
  ```
55
55
 
56
56
  Run `refresh-list` after create/update/delete operations when you need the open
@@ -132,7 +132,7 @@ guesswork:
132
132
  writing. If a named person cannot be resolved unambiguously, clarify in the
133
133
  originating Slack thread; never omit, downgrade, or silently drop the person.
134
134
 
135
- ## Local File Mode
135
+ ## Local Folder Sources
136
136
 
137
137
  Install into an existing repo with:
138
138
 
@@ -140,40 +140,27 @@ Install into an existing repo with:
140
140
  npx @agent-native/core@latest skills add content --mode local-files --scope project
141
141
  ```
142
142
 
143
- The installer copies this skill and writes or updates `agent-native.json` with
144
- Content roots for `docs/`, `blog/`, `content/`, and `resources/`, plus a
145
- `components/` folder for local MDX components. A typical manifest looks like:
143
+ The compatibility spelling still installs the skill, but it no longer enables
144
+ a separate data mode. The CLI writes or updates `agent-native.json` with
145
+ declarative local-folder sources and launches normal database-backed Content.
146
+ A typical root looks like:
146
147
 
147
148
  ```json
148
149
  {
149
150
  "version": 1,
150
151
  "apps": {
151
152
  "content": {
152
- "mode": "local-files",
153
153
  "roots": [
154
154
  {
155
155
  "name": "Docs",
156
156
  "path": "docs",
157
157
  "kind": "docs",
158
- "extensions": [".md", ".mdx"]
159
- },
160
- {
161
- "name": "Blog",
162
- "path": "blog",
163
- "kind": "blog",
164
- "extensions": [".md", ".mdx"]
165
- },
166
- {
167
- "name": "Content",
168
- "path": "content",
169
- "kind": "content",
170
- "extensions": [".md", ".mdx"]
171
- },
172
- {
173
- "name": "Resources",
174
- "path": "resources",
175
- "kind": "resources",
176
- "extensions": [".md", ".mdx"]
158
+ "extensions": [".md", ".mdx"],
159
+ "source": {
160
+ "type": "local-folder",
161
+ "connectionId": "local-folder:<opaque-id>",
162
+ "truthPolicy": "source_primary"
163
+ }
177
164
  }
178
165
  ],
179
166
  "components": "components",
@@ -184,9 +171,10 @@ Content roots for `docs/`, `blog/`, `content/`, and `resources/`, plus a
184
171
  }
185
172
  ```
186
173
 
187
- Local File Mode does not make the host language model local, and the hosted
188
- Content app cannot read private repo files by itself. File access requires a
189
- local Content app, Agent Native Desktop, or another trusted local bridge.
174
+ Content never stores an absolute local path or raw file body in source metadata.
175
+ File access still requires a local Content app, Agent Native Desktop, or another
176
+ trusted bridge. Disconnecting a folder leaves the SQL pages and disk files in
177
+ place. Concurrent edits and missing source files require explicit review.
190
178
 
191
179
  ## MDX And Components
192
180
 
@@ -202,9 +190,8 @@ local Content app, Agent Native Desktop, or another trusted local bridge.
202
190
 
203
191
  ## Boundaries
204
192
 
205
- - Moving, renaming, and reordering local-file pages are not first-class Content
206
- UI operations yet. Use normal file operations when the user asks for those,
207
- then let Content rediscover the file tree.
193
+ - Preserve a page's frontmatter `id` when renaming a source file so the next
194
+ sync recognizes it as the same global Content page.
208
195
  - Do not push/pull Notion, Builder.io, or other provider-backed content unless
209
196
  the user explicitly asks for provider sync.
210
197
  - Do not paste secrets, private provider data, or credential-looking values into
@@ -96,6 +96,11 @@ cd templates/content && pnpm action <name> [args]
96
96
  | `list-documents` | `[--format json]` | List document metadata/tree; no full bodies |
97
97
  | `export-content-source` | `[--format json]` | Export editable docs as `content/*.mdx` source files |
98
98
  | `import-content-source` | `--files <json> [--dryRun true\|false]` | Import `.md`/`.mdx` source files into editable docs |
99
+ | `connect-local-folder-source` | `--connectionId <opaque-id> --label <name> [--spaceId <id>\|--databaseId <id>\|--createSourceBackedSpace true] [--truthPolicy database_primary\|source_primary\|reviewed_bidirectional]` | Connect a trusted browser/Desktop folder registry entry to canonical Files without storing its path or handle in SQL |
100
+ | `sync-local-folder-source` | `--sourceId <id> --files <json> [--dryRun true\|false]` | Materialize a connected folder through Files source rows and record concurrent changes for review |
101
+ | `sync-manifest-local-folder-source` | `--connectionId <opaque-id> [--file <relative-path>] [--spaceId <id>] [--dryRun true\|false]` | Use the trusted local server bridge to materialize a manifest-declared folder into normal SQL-backed Content |
102
+ | `resolve-local-folder-conflict` | `--changeSetId <id> --decision keep_content\|accept_source [--sourceContent <text>]` | Resolve a reviewed folder conflict; accepting requires the exact hashed folder revision |
103
+ | `disconnect-local-folder-source` | `--sourceId <id>` | Disconnect a folder adapter without deleting local files or its SQL-backed Content pages |
99
104
  | `list-builder-docs` | `[--model docs-content\|blog-article] [--limit <n>]` | List Builder docs/blog entries available for `.builder.mdx` pull |
100
105
  | `pull-builder-doc` | `--model <model> --entryId <builder-entry-id> [--dryRun true\|false]` | Pull one Builder entry into Content and return `.builder.mdx` plus `content/builder/.raw` sidecar files |
101
106
  | `check-builder-doc` | `--files <json> [--path <file.builder.mdx>]` or `--documentId <id>` | Validate Builder MDX round-trip, sidecar hashes, and remote conflict status before push |
@@ -112,7 +117,10 @@ cd templates/content && pnpm action <name> [args]
112
117
  | `remove-local-file-source` | `[--sourceRootPath <path>]` | Unlink local-file sources from Content without deleting local Markdown/MDX files |
113
118
  | `list-local-component-files` | | List registered local MDX component source files |
114
119
  | `write-local-component-file` | `--workspaceId <id> --path <relative-component-path> --content <source>` | Create or update a file in a registered local `components/` folder |
115
- | `create-content-database` | `[--documentId <id>] [--parentId <id>] [--title <text>] [--description <text>]` | Create a described database page or convert an existing page into a database |
120
+ | `ensure-content-spaces` | | Idempotently provision the signed-in user's personal and current organization Content spaces, system databases, and catalog references |
121
+ | `backfill-content-files` | | Assign legacy pages to Content spaces and reconcile exactly one canonical Files database membership per non-system page |
122
+ | `list-content-spaces` | | List already-provisioned Content spaces authorized by the signed-in user's current ownership or organization memberships |
123
+ | `create-content-database` | `[--documentId <id>] [--spaceId <id>] [--parentId <id>] [--title <text>] [--description <text>]` | Create a described database page in a Content space or convert an existing page into a database |
116
124
  | `create-inline-content-database` | `--hostDocumentId <id> [--title <text>] [--description <text>]` | Create a described database owned by an inline database block in the host document |
117
125
  | `get-content-database` | `--databaseId <id>` or `--documentId <id>` | Get a database with its description, property/option schema guidance, item pages, and computed ancestry context |
118
126
  | `list-trashed-content-databases` | | List soft-deleted databases visible in the sidebar Trash surface |
@@ -193,37 +201,24 @@ strip of the markdown.
193
201
 
194
202
  ### Local Source Files
195
203
 
196
- Content has two file workflows:
204
+ Content has one database-backed document model with optional folder sources:
197
205
 
198
- - **Database mode local folders:** the `/local-files` view links one or more
206
+ - **Local folder sources:** the `/local-files` view links one or more
199
207
  browser or Agent Native Desktop folders to SQL-backed documents. The UI uses
200
- folder rows: **Pull** reads local `.md`/`.mdx` files through
201
- `import-content-source`, **Check** runs the same import as `--dryRun true`,
202
- and **Push** uses `export-content-source` to write Content documents back to
203
- the chosen folder. Files with known `id` values update existing docs only
204
- when the caller has editor access, and files without ids create new private
205
- docs for the current user. Imported rows keep `source.mode: "local-files"`
206
- and `source.path`, so `list-documents`/`get-document` can distinguish them
207
- from ordinary private pages and the sidebar can show them under Local files.
208
- With one linked folder, imported paths stay flat (`content/page.mdx`); with
209
- multiple linked folders, paths are prefixed by folder name so Local files can
210
- group them by folder. Once a source folder is linked, the selected
211
- `.md`/`.mdx` file is authoritative: opening the page reads the file, editor
212
- saves write the file first, and SQL is updated afterward as
213
- cache/history/search glue. Use `--dryRun true` before a large import when the
214
- source folder may contain unexpected files.
215
- - **Local File Mode editing:** when the app runs with `AGENT_NATIVE_MODE=local-files`
216
- or an `agent-native.json` whose app config enables local files, the standard
217
- Content editor reads and writes configured repo files directly. The left
218
- sidebar is populated from local roots such as `docs/`, `blog/`, `content/`,
219
- and `resources/`; selecting a file opens `/page/<local-file-id>` and
220
- `update-document` writes the selected `.md`/`.mdx` file. The document id is
221
- derived from the file path, and unknown frontmatter is preserved when title,
222
- content, icon, or favorite state changes.
223
- Repos can opt into `profile: "docs/no-bookkeeping"` to keep docs edits from
224
- adding absent `updatedAt`, `icon`, or `isFavorite` frontmatter. Content-only
225
- edits under that profile preserve existing frontmatter but do not create new
226
- bookkeeping keys; explicit title/icon/favorite edits still persist.
208
+ folder rows: **Pull** calls `sync-local-folder-source`, **Check** runs the
209
+ same action with `dryRun: true`, and **Push** uses the source-scoped
210
+ `export-content-source`. Imported files become ordinary SQL documents in the
211
+ target space's canonical Files database. Preserve frontmatter `id` across
212
+ renames; missing files and concurrent changes become reviewable incoming
213
+ change sets instead of silently deleting or overwriting a page. SQL stores
214
+ only opaque connection identity, relative paths, and hashes. Disconnecting a
215
+ folder keeps both the Content pages and disk files.
216
+ - **Manifest/CLI bridge:** `agent-native content local-files <target>` remains
217
+ as a compatibility spelling, but it launches normal database-backed Content.
218
+ `agent-native.json` declares `source.type: "local-folder"` and an opaque
219
+ `connectionId`; it does not select a separate application mode. The trusted
220
+ local server bridge materializes that source through
221
+ `sync-manifest-local-folder-source`.
227
222
  - **Local MDX components:** local file workspaces can expose React components
228
223
  from the configured `components` folder. Export PascalCase components such as
229
224
  `ImpactCounter` from `.tsx` files, then use `<ImpactCounter />` in MDX or pick
@@ -36,6 +36,12 @@ import {
36
36
  type BuilderCmsSourceEntry,
37
37
  } from "./_builder-cms-source-adapter.js";
38
38
  import { executeBuilderCmsWrite } from "./_builder-cms-write-client.js";
39
+ import { ensureDocumentFilesMembership } from "./_content-files.js";
40
+ import {
41
+ organizationContentSpaceId,
42
+ personalContentSpaceId,
43
+ provisionContentSpaces,
44
+ } from "./_content-spaces.js";
39
45
  import { flushOpenDocumentEditorToSql } from "./_document-flush.js";
40
46
 
41
47
  type FetchLike = typeof fetch;
@@ -524,6 +530,17 @@ export async function pullBuilderDocIntoContent(args: {
524
530
  if (!ownerEmail) throw new Error("no authenticated user");
525
531
  const orgId = getRequestOrgId() ?? null;
526
532
  const db = getDb();
533
+ const provisioned = args.dryRun
534
+ ? null
535
+ : await provisionContentSpaces(db, ownerEmail);
536
+ const targetSpaceId = orgId
537
+ ? organizationContentSpaceId(orgId)
538
+ : personalContentSpaceId(ownerEmail);
539
+ if (provisioned && !provisioned.spaceIds.includes(targetSpaceId)) {
540
+ throw new Error(
541
+ "The active organization does not have a writable Content space.",
542
+ );
543
+ }
527
544
  const entry = await readFullBuilderDocsEntry({
528
545
  model: args.model,
529
546
  entryId: args.entryId,
@@ -554,6 +571,9 @@ export async function pullBuilderDocIntoContent(args: {
554
571
  `Requires editor access to update document "${documentId}".`,
555
572
  );
556
573
  }
574
+ if (existing?.spaceId && existing.spaceId !== targetSpaceId) {
575
+ throw new Error("Builder document belongs to a different Content space.");
576
+ }
557
577
 
558
578
  const sourceFields = builderDocumentSourceFields(bundle, now);
559
579
 
@@ -562,6 +582,7 @@ export async function pullBuilderDocIntoContent(args: {
562
582
  await db
563
583
  .update(schema.documents)
564
584
  .set({
585
+ spaceId: existing.spaceId ?? targetSpaceId,
565
586
  title: bundle.mdx.title,
566
587
  content: bundle.mdx.body,
567
588
  updatedAt: now,
@@ -571,6 +592,7 @@ export async function pullBuilderDocIntoContent(args: {
571
592
  } else {
572
593
  await db.insert(schema.documents).values({
573
594
  id: documentId,
595
+ spaceId: targetSpaceId,
574
596
  ownerEmail,
575
597
  orgId,
576
598
  parentId: null,
@@ -586,6 +608,7 @@ export async function pullBuilderDocIntoContent(args: {
586
608
  ...sourceFields,
587
609
  });
588
610
  }
611
+ await ensureDocumentFilesMembership(db, documentId, now);
589
612
  await replaceBuilderDocumentSidecars({
590
613
  db,
591
614
  documentId,
@@ -68,13 +68,31 @@ const notionDatabaseAdapter: ContentDatabaseSourceAdapter = {
68
68
  read: readNotionDatabaseSource,
69
69
  };
70
70
 
71
+ const localFolderAdapter: ContentDatabaseSourceAdapter = {
72
+ sourceType: "local-folder",
73
+ async read() {
74
+ return {
75
+ state: "error",
76
+ entries: [],
77
+ fields: [],
78
+ fetchedAt: new Date().toISOString(),
79
+ message:
80
+ "Local folder refresh requires the trusted browser or Desktop bridge.",
81
+ metadata: { bridgeRequired: true },
82
+ };
83
+ },
84
+ };
85
+
71
86
  const adapters = new Map<
72
87
  ContentDatabaseSourceType,
73
88
  ContentDatabaseSourceAdapter
74
89
  >(
75
- [builderCmsAdapter, localTableAdapter, notionDatabaseAdapter].map(
76
- (adapter) => [adapter.sourceType, adapter],
77
- ),
90
+ [
91
+ builderCmsAdapter,
92
+ localTableAdapter,
93
+ notionDatabaseAdapter,
94
+ localFolderAdapter,
95
+ ].map((adapter) => [adapter.sourceType, adapter]),
78
96
  );
79
97
 
80
98
  export function getContentDatabaseSourceAdapter(
@@ -0,0 +1,338 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { accessFilter } from "@agent-native/core/sharing";
4
+ import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
5
+
6
+ import { schema } from "../server/db/index.js";
7
+ import {
8
+ listContentOrganizationMemberships,
9
+ normalizeContentSpaceEmail,
10
+ resolveContentSpaceAccess,
11
+ } from "./_content-space-access.js";
12
+ import {
13
+ organizationContentSpaceId,
14
+ personalContentSpaceId,
15
+ } from "./_content-spaces.js";
16
+
17
+ type Db = any;
18
+
19
+ export type ContentFilesReconciliation = {
20
+ assignedSpaces: number;
21
+ insertedMemberships: number;
22
+ removedMemberships: number;
23
+ documents: number;
24
+ };
25
+
26
+ export function contentFilesItemId(databaseId: string, documentId: string) {
27
+ return `content_database_item_${createHash("sha256")
28
+ .update(`${databaseId}:${documentId}`)
29
+ .digest("hex")
30
+ .slice(0, 32)}`;
31
+ }
32
+
33
+ async function remapItemReferences(db: Db, replacements: Map<string, string>) {
34
+ if (!replacements.size) return;
35
+ const duplicateIds = [...replacements.keys()];
36
+ const remap = (column: any) => {
37
+ let expression = sql`CASE ${column}`;
38
+ for (const [duplicateId, canonicalId] of replacements)
39
+ expression = sql`${expression} WHEN ${duplicateId} THEN ${canonicalId}`;
40
+ return sql`${expression} ELSE ${column} END`;
41
+ };
42
+ for (const table of [
43
+ schema.contentSpaceCatalogItems,
44
+ schema.contentDatabaseBodyHydrationQueue,
45
+ schema.contentDatabaseSourceRows,
46
+ schema.contentDatabaseSourceChangeSets,
47
+ ]) {
48
+ await db
49
+ .update(table)
50
+ .set({ databaseItemId: remap(table.databaseItemId) })
51
+ .where(inArray(table.databaseItemId, duplicateIds));
52
+ }
53
+ }
54
+
55
+ async function reconcileDocuments(args: {
56
+ db: Db;
57
+ documents: Array<typeof schema.documents.$inferSelect>;
58
+ filesDatabases: Array<typeof schema.contentDatabases.$inferSelect>;
59
+ now: string;
60
+ }) {
61
+ const filesBySpace = new Map(
62
+ args.filesDatabases.map((database) => [database.spaceId, database]),
63
+ );
64
+ const filesDatabaseIds = args.filesDatabases.map((database) => database.id);
65
+ const filesBackingDocumentIds = new Set(
66
+ args.filesDatabases.map((database) => database.documentId),
67
+ );
68
+ const existingItems = filesDatabaseIds.length
69
+ ? await args.db
70
+ .select()
71
+ .from(schema.contentDatabaseItems)
72
+ .where(
73
+ inArray(schema.contentDatabaseItems.databaseId, filesDatabaseIds),
74
+ )
75
+ : [];
76
+ const itemsByDocument = new Map<
77
+ string,
78
+ Array<typeof schema.contentDatabaseItems.$inferSelect>
79
+ >();
80
+ const nextPosition = new Map<string, number>();
81
+ for (const item of existingItems) {
82
+ const documentItems = itemsByDocument.get(item.documentId) ?? [];
83
+ documentItems.push(item);
84
+ itemsByDocument.set(item.documentId, documentItems);
85
+ nextPosition.set(
86
+ item.databaseId,
87
+ Math.max(nextPosition.get(item.databaseId) ?? 0, item.position + 1),
88
+ );
89
+ }
90
+
91
+ const deleteIds = new Set<string>();
92
+ const replacements = new Map<string, string>();
93
+ const inserts: Array<typeof schema.contentDatabaseItems.$inferInsert> = [];
94
+ for (const document of args.documents) {
95
+ const existing = itemsByDocument.get(document.id) ?? [];
96
+ if (filesBackingDocumentIds.has(document.id)) {
97
+ for (const item of existing) deleteIds.add(item.id);
98
+ continue;
99
+ }
100
+ const canonicalDatabase = filesBySpace.get(document.spaceId);
101
+ if (!canonicalDatabase) {
102
+ throw new Error(
103
+ `Content space for document "${document.id}" has no Files database`,
104
+ );
105
+ }
106
+ const canonicalItems = existing
107
+ .filter((item) => item.databaseId === canonicalDatabase.id)
108
+ .sort((left, right) => left.id.localeCompare(right.id));
109
+ const canonicalItemId =
110
+ canonicalItems[0]?.id ??
111
+ contentFilesItemId(canonicalDatabase.id, document.id);
112
+ for (const item of existing) {
113
+ if (item.databaseId !== canonicalDatabase.id) {
114
+ deleteIds.add(item.id);
115
+ replacements.set(item.id, canonicalItemId);
116
+ }
117
+ }
118
+ for (const duplicate of canonicalItems.slice(1)) {
119
+ deleteIds.add(duplicate.id);
120
+ replacements.set(duplicate.id, canonicalItemId);
121
+ }
122
+ if (canonicalItems.length === 0) {
123
+ const position = nextPosition.get(canonicalDatabase.id) ?? 0;
124
+ nextPosition.set(canonicalDatabase.id, position + 1);
125
+ inserts.push({
126
+ id: contentFilesItemId(canonicalDatabase.id, document.id),
127
+ ownerEmail: document.ownerEmail,
128
+ orgId: document.orgId,
129
+ databaseId: canonicalDatabase.id,
130
+ documentId: document.id,
131
+ position,
132
+ createdAt: args.now,
133
+ updatedAt: args.now,
134
+ });
135
+ }
136
+ }
137
+ await remapItemReferences(args.db, replacements);
138
+ if (deleteIds.size) {
139
+ await args.db
140
+ .delete(schema.contentDatabaseItems)
141
+ .where(inArray(schema.contentDatabaseItems.id, [...deleteIds]));
142
+ }
143
+ if (inserts.length) {
144
+ await args.db
145
+ .insert(schema.contentDatabaseItems)
146
+ .values(inserts)
147
+ .onConflictDoNothing();
148
+ }
149
+ return { inserted: inserts.length, removed: deleteIds.size };
150
+ }
151
+
152
+ export async function ensureDocumentFilesMembership(
153
+ db: Db,
154
+ documentId: string,
155
+ now = new Date().toISOString(),
156
+ accessContext?: { userEmail?: string; orgId?: string },
157
+ ) {
158
+ const [document] = await db
159
+ .select()
160
+ .from(schema.documents)
161
+ .where(
162
+ and(
163
+ eq(schema.documents.id, documentId),
164
+ accessFilter(schema.documents, schema.documentShares, accessContext),
165
+ ),
166
+ );
167
+ if (!document) throw new Error(`Document "${documentId}" not found`);
168
+ if (!document.spaceId)
169
+ throw new Error(`Document "${documentId}" does not have a Content space`);
170
+ const filesDatabases = await db
171
+ .select()
172
+ .from(schema.contentDatabases)
173
+ .where(
174
+ and(
175
+ eq(schema.contentDatabases.systemRole, "files"),
176
+ eq(schema.contentDatabases.spaceId, document.spaceId),
177
+ ),
178
+ );
179
+ return reconcileDocuments({ db, documents: [document], filesDatabases, now });
180
+ }
181
+
182
+ export async function ensureDocumentsFilesMembership(
183
+ db: Db,
184
+ documentIds: string[],
185
+ now = new Date().toISOString(),
186
+ ownerEmail?: string,
187
+ ) {
188
+ const ids = [...new Set(documentIds)];
189
+ if (ids.length === 0) return { inserted: 0, removed: 0 };
190
+ const documents = await db
191
+ .select()
192
+ .from(schema.documents)
193
+ .where(
194
+ and(
195
+ inArray(schema.documents.id, ids),
196
+ ownerEmail
197
+ ? or(
198
+ eq(schema.documents.ownerEmail, ownerEmail),
199
+ accessFilter(schema.documents, schema.documentShares),
200
+ )
201
+ : accessFilter(schema.documents, schema.documentShares),
202
+ ),
203
+ );
204
+ if (documents.length !== ids.length) {
205
+ const found = new Set(documents.map((document: any) => document.id));
206
+ const missing = ids.filter((id) => !found.has(id));
207
+ throw new Error(`Documents not found: ${missing.join(", ")}`);
208
+ }
209
+ for (const document of documents) {
210
+ if (!document.spaceId) {
211
+ throw new Error(
212
+ `Document "${document.id}" does not have a Content space`,
213
+ );
214
+ }
215
+ }
216
+ const spaceIds: string[] = Array.from(
217
+ new Set<string>(documents.map((document: any) => String(document.spaceId))),
218
+ );
219
+ const filesDatabases = await db
220
+ .select()
221
+ .from(schema.contentDatabases)
222
+ .where(
223
+ and(
224
+ eq(schema.contentDatabases.systemRole, "files"),
225
+ or(
226
+ ...spaceIds.map((spaceId) =>
227
+ eq(schema.contentDatabases.spaceId, spaceId),
228
+ ),
229
+ ),
230
+ ),
231
+ );
232
+ return reconcileDocuments({ db, documents, filesDatabases, now });
233
+ }
234
+
235
+ export async function reconcileContentFilesMemberships(
236
+ db: Db,
237
+ userEmail: string,
238
+ ): Promise<ContentFilesReconciliation> {
239
+ const email = normalizeContentSpaceEmail(userEmail);
240
+ const memberships = await listContentOrganizationMemberships(email);
241
+ const personalSpaceId = personalContentSpaceId(email);
242
+ const orgSpaceIds = new Map<string, string>();
243
+ for (const membership of memberships) {
244
+ const spaceId = organizationContentSpaceId(membership.orgId);
245
+ try {
246
+ await resolveContentSpaceAccess(spaceId, "editor");
247
+ orgSpaceIds.set(membership.orgId, spaceId);
248
+ } catch (error) {
249
+ if (
250
+ error instanceof Error &&
251
+ error.message ===
252
+ `Editor access is required for Content space "${spaceId}"`
253
+ ) {
254
+ continue;
255
+ }
256
+ throw error;
257
+ }
258
+ }
259
+ const accessibleSpaceIds = [personalSpaceId, ...orgSpaceIds.values()];
260
+ const now = new Date().toISOString();
261
+ const result: ContentFilesReconciliation = {
262
+ assignedSpaces: 0,
263
+ insertedMemberships: 0,
264
+ removedMemberships: 0,
265
+ documents: 0,
266
+ };
267
+
268
+ await db.transaction(async (tx: Db) => {
269
+ const personalLegacyDocuments = await tx
270
+ .select({ id: schema.documents.id })
271
+ .from(schema.documents)
272
+ .where(
273
+ and(
274
+ eq(schema.documents.ownerEmail, email),
275
+ isNull(schema.documents.orgId),
276
+ isNull(schema.documents.spaceId),
277
+ ),
278
+ );
279
+ if (personalLegacyDocuments.length) {
280
+ await tx
281
+ .update(schema.documents)
282
+ .set({ spaceId: personalSpaceId, updatedAt: now })
283
+ .where(
284
+ inArray(
285
+ schema.documents.id,
286
+ personalLegacyDocuments.map((row: any) => row.id),
287
+ ),
288
+ );
289
+ result.assignedSpaces += personalLegacyDocuments.length;
290
+ }
291
+ for (const [orgId, spaceId] of orgSpaceIds) {
292
+ const legacyDocuments = await tx
293
+ .select({ id: schema.documents.id })
294
+ .from(schema.documents)
295
+ .where(
296
+ and(
297
+ eq(schema.documents.orgId, orgId),
298
+ isNull(schema.documents.spaceId),
299
+ ),
300
+ );
301
+ if (!legacyDocuments.length) continue;
302
+ await tx
303
+ .update(schema.documents)
304
+ .set({ spaceId, updatedAt: now })
305
+ .where(
306
+ inArray(
307
+ schema.documents.id,
308
+ legacyDocuments.map((row: any) => row.id),
309
+ ),
310
+ );
311
+ result.assignedSpaces += legacyDocuments.length;
312
+ }
313
+
314
+ const documents = await tx
315
+ .select()
316
+ .from(schema.documents)
317
+ .where(inArray(schema.documents.spaceId, accessibleSpaceIds));
318
+ const filesDatabases = await tx
319
+ .select()
320
+ .from(schema.contentDatabases)
321
+ .where(
322
+ and(
323
+ eq(schema.contentDatabases.systemRole, "files"),
324
+ inArray(schema.contentDatabases.spaceId, accessibleSpaceIds),
325
+ ),
326
+ );
327
+ const membershipResult = await reconcileDocuments({
328
+ db: tx,
329
+ documents,
330
+ filesDatabases,
331
+ now,
332
+ });
333
+ result.documents = documents.length;
334
+ result.insertedMemberships = membershipResult.inserted;
335
+ result.removedMemberships = membershipResult.removed;
336
+ });
337
+ return result;
338
+ }