@agent-native/core 0.102.2 → 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 (206) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -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/local-artifacts/index.ts +65 -0
  31. package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
  32. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
  33. package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
  34. package/corpus/templates/assets/app/i18n-data.ts +215 -0
  35. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
  36. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
  37. package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -0
  38. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +12 -2
  39. package/corpus/templates/clips/app/components/player/settings-panel.tsx +30 -12
  40. package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
  41. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
  42. package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -2
  43. package/corpus/templates/clips/app/i18n/de-DE.ts +4 -2
  44. package/corpus/templates/clips/app/i18n/en-US.ts +3 -2
  45. package/corpus/templates/clips/app/i18n/es-ES.ts +4 -2
  46. package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -2
  47. package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -2
  48. package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -1
  49. package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -2
  50. package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -2
  51. package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -2
  52. package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -2
  53. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +27 -1
  54. package/corpus/templates/clips/server/lib/share-password.ts +6 -3
  55. package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
  56. package/corpus/templates/content/AGENTS.md +25 -30
  57. package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
  58. package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
  59. package/corpus/templates/content/actions/_content-files.ts +338 -0
  60. package/corpus/templates/content/actions/_content-space-access.ts +135 -0
  61. package/corpus/templates/content/actions/_content-spaces.ts +640 -0
  62. package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
  63. package/corpus/templates/content/actions/_database-utils.ts +7 -1
  64. package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
  65. package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
  66. package/corpus/templates/content/actions/_property-utils.ts +9 -4
  67. package/corpus/templates/content/actions/add-database-item.ts +108 -65
  68. package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
  69. package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
  70. package/corpus/templates/content/actions/create-content-database.ts +129 -3
  71. package/corpus/templates/content/actions/create-document.ts +65 -55
  72. package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
  73. package/corpus/templates/content/actions/delete-content-database.ts +3 -0
  74. package/corpus/templates/content/actions/delete-document.ts +7 -11
  75. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
  76. package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
  77. package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
  78. package/corpus/templates/content/actions/edit-document.ts +2 -28
  79. package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
  80. package/corpus/templates/content/actions/export-content-source.ts +47 -9
  81. package/corpus/templates/content/actions/get-document.ts +0 -14
  82. package/corpus/templates/content/actions/import-content-source.ts +43 -3
  83. package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
  84. package/corpus/templates/content/actions/list-documents.ts +12 -9
  85. package/corpus/templates/content/actions/move-document.ts +3 -14
  86. package/corpus/templates/content/actions/pull-document.ts +0 -23
  87. package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
  88. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
  89. package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
  90. package/corpus/templates/content/actions/search-documents.ts +0 -34
  91. package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
  92. package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
  93. package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
  94. package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
  95. package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
  96. package/corpus/templates/content/actions/update-document.ts +0 -15
  97. package/corpus/templates/content/actions/view-screen.ts +2 -50
  98. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
  99. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
  100. package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
  101. package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
  102. package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
  103. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
  104. package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
  105. package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
  106. package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
  107. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
  108. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
  109. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
  110. package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
  111. package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
  112. package/corpus/templates/content/app/i18n-data.ts +75 -0
  113. package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
  114. package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
  115. package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
  116. package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
  117. package/corpus/templates/content/parity/matrix.md +2 -1
  118. package/corpus/templates/content/parity/matrix.ts +37 -1
  119. package/corpus/templates/content/scripts/dev-database.mjs +7 -5
  120. package/corpus/templates/content/server/db/schema.ts +113 -40
  121. package/corpus/templates/content/server/lib/document-context.ts +15 -1
  122. package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
  123. package/corpus/templates/content/server/plugins/db.ts +153 -0
  124. package/corpus/templates/content/shared/api.ts +17 -3
  125. package/corpus/templates/content/shared/builder-mdx.ts +163 -16
  126. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
  127. package/corpus/templates/design/AGENTS.md +6 -1
  128. package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
  129. package/corpus/templates/design/actions/list-design-templates.ts +69 -21
  130. package/corpus/templates/design/actions/navigate.ts +2 -2
  131. package/corpus/templates/design/actions/view-screen.ts +15 -2
  132. package/corpus/templates/design/agent-native.app-skill.json +5 -0
  133. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
  134. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
  135. package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
  136. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
  137. package/corpus/templates/design/app/i18n-data.ts +21 -2
  138. package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
  139. package/corpus/templates/design/app/pages/Index.tsx +195 -24
  140. package/corpus/templates/design/app/pages/Templates.tsx +144 -55
  141. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
  142. package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
  143. package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
  144. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  145. package/corpus/templates/mail/AGENTS.md +23 -18
  146. package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
  147. package/corpus/templates/mail/actions/mark-read.ts +131 -20
  148. package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
  149. package/corpus/templates/mail/actions/move-email.ts +30 -24
  150. package/corpus/templates/mail/actions/send-email.ts +73 -68
  151. package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
  152. package/corpus/templates/mail/server/handlers/emails.ts +218 -209
  153. package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
  154. package/corpus/templates/mail/server/lib/email-state.ts +198 -113
  155. package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
  156. package/corpus/templates/mail/server/lib/jobs.ts +69 -74
  157. package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
  158. package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
  159. package/dist/cli/content-local.d.ts +1 -0
  160. package/dist/cli/content-local.d.ts.map +1 -1
  161. package/dist/cli/content-local.js +75 -17
  162. package/dist/cli/content-local.js.map +1 -1
  163. package/dist/cli/index.js +2 -2
  164. package/dist/cli/index.js.map +1 -1
  165. package/dist/cli/skills-content/content-skill.d.ts +1 -1
  166. package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
  167. package/dist/cli/skills-content/content-skill.js +36 -25
  168. package/dist/cli/skills-content/content-skill.js.map +1 -1
  169. package/dist/cli/skills.d.ts.map +1 -1
  170. package/dist/cli/skills.js +47 -14
  171. package/dist/cli/skills.js.map +1 -1
  172. package/dist/collab/awareness.d.ts +2 -2
  173. package/dist/collab/awareness.d.ts.map +1 -1
  174. package/dist/collab/struct-routes.d.ts +1 -1
  175. package/dist/local-artifacts/index.d.ts +10 -0
  176. package/dist/local-artifacts/index.d.ts.map +1 -1
  177. package/dist/local-artifacts/index.js +39 -0
  178. package/dist/local-artifacts/index.js.map +1 -1
  179. package/dist/notifications/routes.d.ts +1 -1
  180. package/dist/observability/routes.d.ts +5 -5
  181. package/dist/resources/handlers.d.ts +1 -1
  182. package/dist/secrets/routes.d.ts +9 -9
  183. package/docs/content/local-file-mode.mdx +99 -489
  184. package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  185. package/docs/content/locales/ar-SA/template-content.mdx +10 -24
  186. package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  187. package/docs/content/locales/de-DE/template-content.mdx +10 -24
  188. package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  189. package/docs/content/locales/es-ES/template-content.mdx +10 -24
  190. package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  191. package/docs/content/locales/fr-FR/template-content.mdx +10 -24
  192. package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  193. package/docs/content/locales/hi-IN/template-content.mdx +10 -24
  194. package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  195. package/docs/content/locales/ja-JP/template-content.mdx +10 -24
  196. package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  197. package/docs/content/locales/ko-KR/template-content.mdx +10 -24
  198. package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  199. package/docs/content/locales/pt-BR/template-content.mdx +10 -24
  200. package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  201. package/docs/content/locales/zh-CN/template-content.mdx +9 -24
  202. package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  203. package/docs/content/locales/zh-TW/template-content.mdx +9 -24
  204. package/docs/content/template-content.mdx +26 -42
  205. package/package.json +1 -1
  206. package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
@@ -0,0 +1,221 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { writeAppState } from "@agent-native/core/application-state";
3
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
+ import { and, eq } from "drizzle-orm";
5
+ import { z } from "zod";
6
+
7
+ import { getDb, schema } from "../server/db/index.js";
8
+ import type { ContentDatabaseSourceTruthPolicy } from "../shared/api.js";
9
+ import {
10
+ normalizeContentSpaceEmail,
11
+ resolveContentSpaceAccess,
12
+ } from "./_content-space-access.js";
13
+ import {
14
+ personalContentSpaceId,
15
+ provisionContentSpaces,
16
+ provisionSourceBackedContentSpace,
17
+ } from "./_content-spaces.js";
18
+ import {
19
+ LOCAL_FOLDER_SOURCE_TYPE,
20
+ localFolderSourceCapabilities,
21
+ localFolderSourceId,
22
+ localFolderSourceMetadata,
23
+ } from "./_local-folder-source.js";
24
+
25
+ const truthPolicySchema = z
26
+ .enum(["database_primary", "source_primary", "reviewed_bidirectional"])
27
+ .default("database_primary");
28
+
29
+ export default defineAction({
30
+ description:
31
+ "Connect an opaque trusted local-folder handle to a canonical Files database. The browser or Desktop bridge keeps the real handle/path; Content stores only safe connection metadata.",
32
+ schema: z.object({
33
+ connectionId: z
34
+ .string()
35
+ .min(1)
36
+ .max(300)
37
+ .describe("Stable opaque ID from the trusted browser/Desktop registry"),
38
+ label: z.string().min(1).max(200).describe("User-visible folder label"),
39
+ spaceId: z
40
+ .string()
41
+ .optional()
42
+ .describe(
43
+ "Existing Content space whose Files database receives the source",
44
+ ),
45
+ databaseId: z
46
+ .string()
47
+ .optional()
48
+ .describe("Existing canonical Files database receiving the source"),
49
+ createSourceBackedSpace: z
50
+ .boolean()
51
+ .optional()
52
+ .default(false)
53
+ .describe("Create a separate private Content space for this folder"),
54
+ truthPolicy: truthPolicySchema.describe(
55
+ "Whether Content, the folder, or reviewed conflict resolution is authoritative",
56
+ ),
57
+ dryRun: z.boolean().optional().default(false),
58
+ }),
59
+ run: async (args) => {
60
+ const userEmail = getRequestUserEmail();
61
+ if (!userEmail) throw new Error("no authenticated user");
62
+ if (args.spaceId && args.databaseId) {
63
+ throw new Error("Choose either spaceId or databaseId, not both");
64
+ }
65
+ if (args.createSourceBackedSpace && (args.spaceId || args.databaseId)) {
66
+ throw new Error(
67
+ "A new source-backed space cannot also target an existing space or database",
68
+ );
69
+ }
70
+
71
+ const db = getDb();
72
+ const connectionId = args.connectionId.trim();
73
+ const label = args.label.trim();
74
+ const truthPolicy = args.truthPolicy as ContentDatabaseSourceTruthPolicy;
75
+ if (args.dryRun) {
76
+ const [existing] = await db
77
+ .select({
78
+ source: schema.contentDatabaseSources,
79
+ database: schema.contentDatabases,
80
+ })
81
+ .from(schema.contentDatabaseSources)
82
+ .innerJoin(
83
+ schema.contentDatabases,
84
+ eq(
85
+ schema.contentDatabases.id,
86
+ schema.contentDatabaseSources.databaseId,
87
+ ),
88
+ )
89
+ .where(
90
+ and(
91
+ eq(
92
+ schema.contentDatabaseSources.sourceType,
93
+ LOCAL_FOLDER_SOURCE_TYPE,
94
+ ),
95
+ eq(schema.contentDatabaseSources.sourceTable, connectionId),
96
+ eq(
97
+ schema.contentDatabaseSources.ownerEmail,
98
+ normalizeContentSpaceEmail(userEmail),
99
+ ),
100
+ ),
101
+ );
102
+ if (!existing?.database.spaceId) {
103
+ return {
104
+ connected: false as const,
105
+ sourceId: null,
106
+ connectionId,
107
+ label,
108
+ truthPolicy,
109
+ spaceId: null,
110
+ filesDatabaseId: null,
111
+ };
112
+ }
113
+ await resolveContentSpaceAccess(existing.database.spaceId, "editor");
114
+ return {
115
+ connected: true as const,
116
+ sourceId: existing.source.id,
117
+ sourceType: LOCAL_FOLDER_SOURCE_TYPE,
118
+ connectionId,
119
+ label,
120
+ truthPolicy,
121
+ spaceId: existing.database.spaceId,
122
+ filesDatabaseId: existing.database.id,
123
+ };
124
+ }
125
+ await provisionContentSpaces(db, userEmail);
126
+
127
+ let targetSpaceId = args.spaceId;
128
+ let targetDatabaseId = args.databaseId;
129
+ if (args.createSourceBackedSpace) {
130
+ const provisioned = await provisionSourceBackedContentSpace(
131
+ db,
132
+ userEmail,
133
+ { connectionId, name: label },
134
+ );
135
+ targetSpaceId = provisioned.spaceId;
136
+ targetDatabaseId = provisioned.filesDatabaseId;
137
+ } else if (!targetSpaceId && !targetDatabaseId) {
138
+ targetSpaceId = personalContentSpaceId(userEmail);
139
+ }
140
+
141
+ let database: typeof schema.contentDatabases.$inferSelect | undefined;
142
+ if (targetDatabaseId) {
143
+ [database] = await db
144
+ .select()
145
+ .from(schema.contentDatabases)
146
+ .where(eq(schema.contentDatabases.id, targetDatabaseId));
147
+ if (!database || database.deletedAt || database.systemRole !== "files") {
148
+ throw new Error(
149
+ `Database "${targetDatabaseId}" is not a canonical Files database`,
150
+ );
151
+ }
152
+ if (!database.spaceId) {
153
+ throw new Error(`Database "${targetDatabaseId}" has no Content space`);
154
+ }
155
+ targetSpaceId = database.spaceId;
156
+ }
157
+ if (!targetSpaceId) throw new Error("Content space is required");
158
+
159
+ await resolveContentSpaceAccess(targetSpaceId, "editor");
160
+ if (!database) {
161
+ [database] = await db
162
+ .select()
163
+ .from(schema.contentDatabases)
164
+ .where(
165
+ and(
166
+ eq(schema.contentDatabases.spaceId, targetSpaceId),
167
+ eq(schema.contentDatabases.systemRole, "files"),
168
+ ),
169
+ );
170
+ }
171
+ if (!database || database.deletedAt) {
172
+ throw new Error(`Content space "${targetSpaceId}" has no Files database`);
173
+ }
174
+
175
+ const id = localFolderSourceId(database.id, connectionId);
176
+ const now = new Date().toISOString();
177
+ const values = {
178
+ id,
179
+ ownerEmail: database.ownerEmail,
180
+ orgId: database.orgId,
181
+ databaseId: database.id,
182
+ sourceType: LOCAL_FOLDER_SOURCE_TYPE,
183
+ sourceName: label,
184
+ sourceTable: connectionId,
185
+ syncState: "linked",
186
+ freshness: "unknown",
187
+ capabilitiesJson: JSON.stringify(localFolderSourceCapabilities()),
188
+ metadataJson: JSON.stringify(
189
+ localFolderSourceMetadata({ connectionId, label, truthPolicy }),
190
+ ),
191
+ lastError: null,
192
+ createdAt: now,
193
+ updatedAt: now,
194
+ };
195
+ await db
196
+ .insert(schema.contentDatabaseSources)
197
+ .values(values)
198
+ .onConflictDoNothing();
199
+ await db
200
+ .update(schema.contentDatabaseSources)
201
+ .set({
202
+ sourceName: label,
203
+ capabilitiesJson: values.capabilitiesJson,
204
+ metadataJson: values.metadataJson,
205
+ updatedAt: now,
206
+ })
207
+ .where(eq(schema.contentDatabaseSources.id, id));
208
+
209
+ await writeAppState("refresh-signal", { ts: Date.now() });
210
+ return {
211
+ connected: true as const,
212
+ sourceId: id,
213
+ sourceType: LOCAL_FOLDER_SOURCE_TYPE,
214
+ connectionId,
215
+ label,
216
+ truthPolicy,
217
+ spaceId: targetSpaceId,
218
+ filesDatabaseId: database.id,
219
+ };
220
+ },
221
+ });
@@ -14,6 +14,12 @@ import type {
14
14
  ContentDatabaseResponse,
15
15
  CreateDatabaseRequest,
16
16
  } from "../shared/api.js";
17
+ import { ensureDocumentFilesMembership } from "./_content-files.js";
18
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
19
+ import {
20
+ organizationContentSpaceId,
21
+ provisionContentSpaces,
22
+ } from "./_content-spaces.js";
17
23
  import { getContentDatabaseResponse } from "./_database-utils.js";
18
24
  import { documentsPositionScope, withPositionLock } from "./_position-utils.js";
19
25
  import { nanoid, seedDefaultBlocksField } from "./_property-utils.js";
@@ -23,6 +29,10 @@ const createContentDatabaseSchema = z.object({
23
29
  .string()
24
30
  .optional()
25
31
  .describe("Existing document to convert into a database page"),
32
+ spaceId: z
33
+ .string()
34
+ .optional()
35
+ .describe("Content space for a new top-level database"),
26
36
  parentId: z
27
37
  .string()
28
38
  .nullish()
@@ -73,13 +83,84 @@ export async function createContentDatabaseCore(
73
83
  args: CreateDatabaseRequest,
74
84
  options: { db?: any } = {},
75
85
  ): Promise<ContentDatabaseResponse> {
76
- const databaseId = await createContentDatabaseRecord(args, options);
86
+ const db = options.db ?? getDb();
87
+ const resolvedSpaceId = await resolveContentDatabaseSpace(args, db);
88
+ let databaseId: string | null = null;
89
+ if (options.db) {
90
+ databaseId = await createContentDatabaseRecord(args, {
91
+ db,
92
+ spaceId: resolvedSpaceId,
93
+ });
94
+ } else {
95
+ await db.transaction(async (tx: any) => {
96
+ databaseId = await createContentDatabaseRecord(args, {
97
+ db: tx,
98
+ spaceId: resolvedSpaceId,
99
+ });
100
+ });
101
+ }
102
+ if (!databaseId) throw new Error("Content database was not created");
77
103
  return getContentDatabaseResponse(databaseId);
78
104
  }
79
105
 
106
+ async function healLegacyDocumentSpace(db: any, resource: any) {
107
+ const provisioned = await provisionContentSpaces(db, resource.ownerEmail);
108
+ const spaceId = resource.orgId
109
+ ? organizationContentSpaceId(resource.orgId)
110
+ : provisioned.personalSpaceId;
111
+ const [space] = await db
112
+ .select({ id: schema.contentSpaces.id })
113
+ .from(schema.contentSpaces)
114
+ .where(eq(schema.contentSpaces.id, spaceId));
115
+ if (!space) {
116
+ throw new Error(`Unable to resolve a Content space for "${resource.id}"`);
117
+ }
118
+ const now = new Date().toISOString();
119
+ await db
120
+ .update(schema.documents)
121
+ .set({ spaceId, updatedAt: now })
122
+ .where(eq(schema.documents.id, resource.id));
123
+ await ensureDocumentFilesMembership(db, resource.id, now);
124
+ return spaceId;
125
+ }
126
+
127
+ export async function resolveContentDatabaseSpace(
128
+ args: CreateDatabaseRequest,
129
+ db: any,
130
+ ): Promise<string> {
131
+ if (args.documentId) {
132
+ const access = await assertAccess("document", args.documentId, "editor");
133
+ const spaceId =
134
+ (access.resource.spaceId as string | null) ??
135
+ (await healLegacyDocumentSpace(db, access.resource));
136
+ if (args.spaceId && args.spaceId !== spaceId) {
137
+ throw new Error(
138
+ "A converted database must keep its document Content space",
139
+ );
140
+ }
141
+ return spaceId;
142
+ }
143
+ if (args.parentId) {
144
+ const access = await assertAccess("document", args.parentId, "editor");
145
+ const spaceId =
146
+ (access.resource.spaceId as string | null) ??
147
+ (await healLegacyDocumentSpace(db, access.resource));
148
+ if (args.spaceId && args.spaceId !== spaceId) {
149
+ throw new Error("Nested databases must use their parent Content space");
150
+ }
151
+ return spaceId;
152
+ }
153
+ const userEmail = getRequestUserEmail();
154
+ if (!userEmail) throw new Error("no authenticated user");
155
+ const provisioned = await provisionContentSpaces(db, userEmail);
156
+ const spaceId = args.spaceId ?? provisioned.personalSpaceId;
157
+ await resolveContentSpaceAccess(spaceId, "editor");
158
+ return spaceId;
159
+ }
160
+
80
161
  export async function createContentDatabaseRecord(
81
162
  args: CreateDatabaseRequest,
82
- options: { db?: any } = {},
163
+ options: { db?: any; spaceId?: string } = {},
83
164
  ): Promise<string> {
84
165
  const db = options.db ?? getDb();
85
166
  const now = new Date().toISOString();
@@ -89,6 +170,7 @@ export async function createContentDatabaseRecord(
89
170
  let ownerEmail = getRequestUserEmail();
90
171
  if (!ownerEmail) throw new Error("no authenticated user");
91
172
  let orgId = getRequestOrgId() ?? null;
173
+ let spaceId = options.spaceId ?? null;
92
174
  let inheritedShares: Array<{
93
175
  principalType: "user" | "org";
94
176
  principalId: string;
@@ -100,13 +182,34 @@ export async function createContentDatabaseRecord(
100
182
  const document = access.resource;
101
183
  ownerEmail = document.ownerEmail as string;
102
184
  orgId = (document.orgId as string | null) ?? null;
185
+ spaceId = (document.spaceId as string | null) ?? spaceId;
186
+ if (!spaceId) {
187
+ throw new Error(`Document "${documentId}" has no Content space`);
188
+ }
189
+ if (args.spaceId && args.spaceId !== spaceId) {
190
+ throw new Error(
191
+ "A converted database must keep its document Content space",
192
+ );
193
+ }
103
194
  title = databaseTitleForPage(title, document.title);
104
195
 
105
196
  const [existing] = await db
106
197
  .select()
107
198
  .from(schema.contentDatabases)
108
199
  .where(eq(schema.contentDatabases.documentId, documentId));
109
- if (existing) return existing.id;
200
+ if (existing) {
201
+ if (existing.spaceId !== spaceId) {
202
+ await db
203
+ .update(schema.contentDatabases)
204
+ .set({ spaceId, updatedAt: now })
205
+ .where(eq(schema.contentDatabases.id, existing.id));
206
+ }
207
+ await ensureDocumentFilesMembership(db, documentId, now, {
208
+ userEmail: getRequestUserEmail(),
209
+ orgId: orgId ?? undefined,
210
+ });
211
+ return existing.id;
212
+ }
110
213
 
111
214
  if (title && title !== document.title && !document.title.trim()) {
112
215
  await db
@@ -131,6 +234,13 @@ export async function createContentDatabaseRecord(
131
234
  const parent = parentAccess.resource;
132
235
  ownerEmail = parent.ownerEmail as string;
133
236
  orgId = (parent.orgId as string | null) ?? null;
237
+ spaceId = (parent.spaceId as string | null) ?? spaceId;
238
+ if (!spaceId) {
239
+ throw new Error(`Parent document "${parentId}" has no Content space`);
240
+ }
241
+ if (args.spaceId && args.spaceId !== spaceId) {
242
+ throw new Error("Nested databases must use their parent Content space");
243
+ }
134
244
  visibility = parent.visibility ?? "private";
135
245
  hideFromSearch = parent.hideFromSearch ?? 0;
136
246
  inheritedShares = await db
@@ -141,6 +251,16 @@ export async function createContentDatabaseRecord(
141
251
  })
142
252
  .from(schema.documentShares)
143
253
  .where(eq(schema.documentShares.resourceId, parentId));
254
+ } else {
255
+ if (!spaceId) {
256
+ throw new Error(
257
+ "A top-level database requires a resolved Content space",
258
+ );
259
+ }
260
+ const spaceAccess = await resolveContentSpaceAccess(spaceId, "editor");
261
+ ownerEmail = getRequestUserEmail() ?? ownerEmail;
262
+ orgId = spaceAccess.space.orgId;
263
+ visibility = orgId ? "org" : "private";
144
264
  }
145
265
 
146
266
  documentId = nanoid();
@@ -168,6 +288,7 @@ export async function createContentDatabaseRecord(
168
288
 
169
289
  await db.insert(schema.documents).values({
170
290
  id: documentId!,
291
+ spaceId,
171
292
  ownerEmail: resolvedOwnerEmail,
172
293
  orgId,
173
294
  parentId,
@@ -203,6 +324,7 @@ export async function createContentDatabaseRecord(
203
324
  const databaseId = nanoid();
204
325
  await db.insert(schema.contentDatabases).values({
205
326
  id: databaseId,
327
+ spaceId,
206
328
  ownerEmail,
207
329
  orgId,
208
330
  documentId,
@@ -214,6 +336,10 @@ export async function createContentDatabaseRecord(
214
336
  // Every database is seeded with one primary "Content" Blocks field, backed
215
337
  // by `documents.content`, so each row's body is a first-class property.
216
338
  await seedDefaultBlocksField({ databaseId, ownerEmail, orgId, now, db });
339
+ await ensureDocumentFilesMembership(db, documentId, now, {
340
+ userEmail: getRequestUserEmail(),
341
+ orgId: orgId ?? undefined,
342
+ });
217
343
 
218
344
  return databaseId;
219
345
  }
@@ -14,10 +14,9 @@ import {
14
14
  parseDocumentFavorite,
15
15
  parseDocumentHideFromSearch,
16
16
  } from "../server/lib/documents.js";
17
- import {
18
- createLocalFileDocument,
19
- isContentLocalFileMode,
20
- } from "./_local-file-documents.js";
17
+ import { ensureDocumentFilesMembership } from "./_content-files.js";
18
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
19
+ import { provisionContentSpaces } from "./_content-spaces.js";
21
20
  import { documentsPositionScope, withPositionLock } from "./_position-utils.js";
22
21
 
23
22
  function nanoid(size = 12): string {
@@ -29,13 +28,6 @@ function nanoid(size = 12): string {
29
28
  return id;
30
29
  }
31
30
 
32
- function assertCanWriteAppState() {
33
- if (getRequestUserEmail() || process.env.AGENT_USER_EMAIL) return;
34
- throw new Error(
35
- "Application state access requires an authenticated request context or AGENT_USER_EMAIL env var",
36
- );
37
- }
38
-
39
31
  export default defineAction({
40
32
  description: "Create a new document.",
41
33
  schema: z.object({
@@ -43,6 +35,10 @@ export default defineAction({
43
35
  .string()
44
36
  .optional()
45
37
  .describe("Pre-generated document ID (for optimistic UI)"),
38
+ spaceId: z
39
+ .string()
40
+ .optional()
41
+ .describe("Content space for a new top-level document"),
46
42
  title: z.string().describe("Document title"),
47
43
  content: z.string().optional().describe("Markdown content"),
48
44
  description: z
@@ -66,21 +62,6 @@ export default defineAction({
66
62
  }),
67
63
  },
68
64
  run: async (args) => {
69
- if (await isContentLocalFileMode()) {
70
- assertCanWriteAppState();
71
- const doc = await createLocalFileDocument(args);
72
- await writeAppState("refresh-signal", { ts: Date.now() });
73
- return {
74
- ...doc,
75
- urlPath: `/page/${doc.id}`,
76
- deepLink: buildDeepLink({
77
- app: "content",
78
- view: "editor",
79
- params: { documentId: doc.id },
80
- }),
81
- };
82
- }
83
-
84
65
  const title = args.title;
85
66
 
86
67
  let content = args.content || "";
@@ -130,6 +111,28 @@ export default defineAction({
130
111
  .where(eq(schema.documentShares.resourceId, parentId));
131
112
  }
132
113
 
114
+ let spaceId: string;
115
+ if (parentId) {
116
+ const [parent] = await db
117
+ .select({ spaceId: schema.documents.spaceId })
118
+ .from(schema.documents)
119
+ .where(eq(schema.documents.id, parentId));
120
+ if (!parent?.spaceId) {
121
+ throw new Error(`Parent document "${parentId}" has no Content space`);
122
+ }
123
+ if (args.spaceId && args.spaceId !== parent.spaceId) {
124
+ throw new Error("Nested documents must use their parent Content space");
125
+ }
126
+ spaceId = parent.spaceId;
127
+ } else {
128
+ const provisioned = await provisionContentSpaces(db, currentUserEmail);
129
+ spaceId = args.spaceId ?? provisioned.personalSpaceId;
130
+ const spaceAccess = await resolveContentSpaceAccess(spaceId, "editor");
131
+ ownerEmail = currentUserEmail;
132
+ orgId = spaceAccess.space.orgId;
133
+ visibility = orgId ? "org" : "private";
134
+ }
135
+
133
136
  const now = new Date().toISOString();
134
137
  const id = args.id || nanoid();
135
138
 
@@ -154,39 +157,46 @@ export default defineAction({
154
157
 
155
158
  const position = (maxPos[0]?.max ?? -1) + 1;
156
159
 
157
- await db.insert(schema.documents).values({
158
- id,
159
- ownerEmail,
160
- orgId,
161
- parentId,
162
- title,
163
- content,
164
- description,
165
- icon,
166
- position,
167
- isFavorite: 0,
168
- hideFromSearch,
169
- visibility,
170
- createdAt: now,
171
- updatedAt: now,
160
+ await db.transaction(async (tx) => {
161
+ await tx.insert(schema.documents).values({
162
+ id,
163
+ spaceId,
164
+ ownerEmail,
165
+ orgId,
166
+ parentId,
167
+ title,
168
+ content,
169
+ description,
170
+ icon,
171
+ position,
172
+ isFavorite: 0,
173
+ hideFromSearch,
174
+ visibility,
175
+ createdAt: now,
176
+ updatedAt: now,
177
+ });
178
+
179
+ if (inheritedShares.length > 0) {
180
+ await tx.insert(schema.documentShares).values(
181
+ inheritedShares.map((share) => ({
182
+ id: nanoid(),
183
+ resourceId: id,
184
+ principalType: share.principalType,
185
+ principalId: share.principalId,
186
+ role: share.role,
187
+ createdBy: currentUserEmail,
188
+ createdAt: now,
189
+ })),
190
+ );
191
+ }
192
+ await ensureDocumentFilesMembership(tx, id, now, {
193
+ userEmail: currentUserEmail,
194
+ orgId: orgId ?? undefined,
195
+ });
172
196
  });
173
197
  },
174
198
  );
175
199
 
176
- if (inheritedShares.length > 0) {
177
- await db.insert(schema.documentShares).values(
178
- inheritedShares.map((share) => ({
179
- id: nanoid(),
180
- resourceId: id,
181
- principalType: share.principalType,
182
- principalId: share.principalId,
183
- role: share.role,
184
- createdBy: currentUserEmail,
185
- createdAt: now,
186
- })),
187
- );
188
- }
189
-
190
200
  const [doc] = await db
191
201
  .select()
192
202
  .from(schema.documents)
@@ -10,6 +10,7 @@ import { nanoid } from "./_property-utils.js";
10
10
  import {
11
11
  createContentDatabaseRecord,
12
12
  databaseTitleForPage,
13
+ resolveContentDatabaseSpace,
13
14
  } from "./create-content-database.js";
14
15
 
15
16
  function createInlineDatabaseBlockId(): string {
@@ -33,6 +34,10 @@ export default defineAction({
33
34
  const ownerBlockId = createInlineDatabaseBlockId();
34
35
  let databaseId: string | null = null;
35
36
  let databaseDocumentId: string | null = null;
37
+ const spaceId = await resolveContentDatabaseSpace(
38
+ { parentId: hostDocumentId },
39
+ db,
40
+ );
36
41
 
37
42
  await db.transaction(async (tx) => {
38
43
  databaseId = await createContentDatabaseRecord(
@@ -41,7 +46,7 @@ export default defineAction({
41
46
  title: databaseTitleForPage(title),
42
47
  description,
43
48
  },
44
- { db: tx },
49
+ { db: tx, spaceId },
45
50
  );
46
51
 
47
52
  const [updated] = await tx
@@ -14,6 +14,9 @@ export default defineAction({
14
14
  }),
15
15
  run: async ({ databaseId }) => {
16
16
  const { database } = await assertContentDatabaseLifecycleAccess(databaseId);
17
+ if (database.systemRole) {
18
+ throw new Error("System Content databases cannot be deleted");
19
+ }
17
20
  const db = getDb();
18
21
  const deletedAt = database.deletedAt ?? new Date().toISOString();
19
22
 
@@ -6,11 +6,6 @@ import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
8
  import { chunks } from "./_batch-utils.js";
9
- import {
10
- deleteLocalFileDocument,
11
- isLocalDocumentId,
12
- isContentLocalFileMode,
13
- } from "./_local-file-documents.js";
14
9
 
15
10
  const DELETE_BATCH_SIZE = 90;
16
11
 
@@ -365,16 +360,17 @@ export default defineAction({
365
360
  const id = args.id;
366
361
  if (!id) throw new Error("--id is required");
367
362
 
368
- if ((await isContentLocalFileMode()) && isLocalDocumentId(id)) {
369
- const result = await deleteLocalFileDocument(id);
370
- await writeAppState("refresh-signal", { ts: Date.now() });
371
- return result;
372
- }
373
-
374
363
  const access = await assertAccess("document", id, "admin");
375
364
  const existing = access.resource;
376
365
 
377
366
  const db = getDb();
367
+ const [systemDatabase] = await db
368
+ .select({ systemRole: schema.contentDatabases.systemRole })
369
+ .from(schema.contentDatabases)
370
+ .where(eq(schema.contentDatabases.documentId, id));
371
+ if (systemDatabase?.systemRole) {
372
+ throw new Error("System Content database documents cannot be deleted");
373
+ }
378
374
  const deleted = await deleteDocumentRecursive(
379
375
  db,
380
376
  id,