@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,175 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { writeAppState } from "@agent-native/core/application-state";
3
+ import { and, eq, inArray, ne } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
8
+ import { LOCAL_FOLDER_SOURCE_TYPE } from "./_local-folder-source.js";
9
+
10
+ export default defineAction({
11
+ description:
12
+ "Disconnect a local-folder source without deleting its local files or the SQL-backed Content pages it materialized.",
13
+ schema: z.object({ sourceId: z.string().min(1) }),
14
+ run: async ({ sourceId }) => {
15
+ const db = getDb();
16
+ const [target] = await db
17
+ .select({
18
+ source: schema.contentDatabaseSources,
19
+ database: schema.contentDatabases,
20
+ })
21
+ .from(schema.contentDatabaseSources)
22
+ .innerJoin(
23
+ schema.contentDatabases,
24
+ eq(
25
+ schema.contentDatabases.id,
26
+ schema.contentDatabaseSources.databaseId,
27
+ ),
28
+ )
29
+ .where(eq(schema.contentDatabaseSources.id, sourceId));
30
+ if (
31
+ !target ||
32
+ target.source.sourceType !== LOCAL_FOLDER_SOURCE_TYPE ||
33
+ !target.database.spaceId
34
+ ) {
35
+ throw new Error(`Local folder source "${sourceId}" not found`);
36
+ }
37
+ await resolveContentSpaceAccess(target.database.spaceId, "editor");
38
+ const spaceId = target.database.spaceId;
39
+ let disconnectedDocuments = 0;
40
+ const now = new Date().toISOString();
41
+
42
+ await db.transaction(async (tx: any) => {
43
+ const rows = await tx
44
+ .select({ documentId: schema.contentDatabaseSourceRows.documentId })
45
+ .from(schema.contentDatabaseSourceRows)
46
+ .where(eq(schema.contentDatabaseSourceRows.sourceId, sourceId));
47
+ const documentIds: string[] = [
48
+ ...new Set<string>(rows.map((row: any) => String(row.documentId))),
49
+ ];
50
+ disconnectedDocuments = documentIds.length;
51
+ await tx
52
+ .delete(schema.contentDatabaseSourceExecutions)
53
+ .where(eq(schema.contentDatabaseSourceExecutions.sourceId, sourceId));
54
+ await tx
55
+ .delete(schema.contentDatabaseSourceExecutionClaims)
56
+ .where(
57
+ eq(schema.contentDatabaseSourceExecutionClaims.sourceId, sourceId),
58
+ );
59
+ await tx
60
+ .delete(schema.contentDatabaseSourceChangeReviews)
61
+ .where(
62
+ eq(schema.contentDatabaseSourceChangeReviews.sourceId, sourceId),
63
+ );
64
+ await tx
65
+ .delete(schema.contentDatabaseSourceChangeSets)
66
+ .where(eq(schema.contentDatabaseSourceChangeSets.sourceId, sourceId));
67
+ await tx
68
+ .delete(schema.contentDatabaseBodyHydrationQueue)
69
+ .where(eq(schema.contentDatabaseBodyHydrationQueue.sourceId, sourceId));
70
+ await tx
71
+ .delete(schema.contentDatabaseSourceFields)
72
+ .where(eq(schema.contentDatabaseSourceFields.sourceId, sourceId));
73
+ await tx
74
+ .delete(schema.contentDatabaseSourceRows)
75
+ .where(eq(schema.contentDatabaseSourceRows.sourceId, sourceId));
76
+ await tx
77
+ .delete(schema.contentDatabaseSources)
78
+ .where(eq(schema.contentDatabaseSources.id, sourceId));
79
+ type RemainingLocalRow = {
80
+ documentId: string;
81
+ sourceDisplayKey: string | null;
82
+ sourceValuesJson: string | null;
83
+ sourceName: string;
84
+ };
85
+ const remainingLocalRows: RemainingLocalRow[] = documentIds.length
86
+ ? await tx
87
+ .select({
88
+ documentId: schema.contentDatabaseSourceRows.documentId,
89
+ sourceDisplayKey:
90
+ schema.contentDatabaseSourceRows.sourceDisplayKey,
91
+ sourceValuesJson:
92
+ schema.contentDatabaseSourceRows.sourceValuesJson,
93
+ sourceName: schema.contentDatabaseSources.sourceName,
94
+ })
95
+ .from(schema.contentDatabaseSourceRows)
96
+ .innerJoin(
97
+ schema.contentDatabaseSources,
98
+ eq(
99
+ schema.contentDatabaseSources.id,
100
+ schema.contentDatabaseSourceRows.sourceId,
101
+ ),
102
+ )
103
+ .where(
104
+ and(
105
+ inArray(
106
+ schema.contentDatabaseSourceRows.documentId,
107
+ documentIds,
108
+ ),
109
+ ne(schema.contentDatabaseSourceRows.sourceId, sourceId),
110
+ eq(
111
+ schema.contentDatabaseSources.sourceType,
112
+ LOCAL_FOLDER_SOURCE_TYPE,
113
+ ),
114
+ ),
115
+ )
116
+ : [];
117
+ const remainingLocalRowByDocument = new Map<string, RemainingLocalRow>(
118
+ remainingLocalRows.map((row) => [row.documentId, row]),
119
+ );
120
+ const documentsToClear = documentIds.filter(
121
+ (documentId) => !remainingLocalRowByDocument.has(documentId),
122
+ );
123
+ if (documentsToClear.length) {
124
+ await tx
125
+ .update(schema.documents)
126
+ .set({
127
+ sourceMode: "database",
128
+ sourceKind: null,
129
+ sourcePath: null,
130
+ sourceRootPath: null,
131
+ sourceUpdatedAt: now,
132
+ updatedAt: now,
133
+ })
134
+ .where(
135
+ and(
136
+ inArray(schema.documents.id, documentsToClear),
137
+ eq(schema.documents.spaceId, spaceId),
138
+ ),
139
+ );
140
+ }
141
+ for (const [documentId, remaining] of remainingLocalRowByDocument) {
142
+ const sourceValues = JSON.parse(remaining.sourceValuesJson || "{}") as {
143
+ relativePath?: unknown;
144
+ };
145
+ const relativePath =
146
+ typeof sourceValues.relativePath === "string"
147
+ ? sourceValues.relativePath
148
+ : remaining.sourceDisplayKey;
149
+ await tx
150
+ .update(schema.documents)
151
+ .set({
152
+ sourceMode: "local-files",
153
+ sourceKind: "file",
154
+ sourcePath: relativePath,
155
+ sourceRootPath: remaining.sourceName,
156
+ sourceUpdatedAt: now,
157
+ updatedAt: now,
158
+ })
159
+ .where(
160
+ and(
161
+ eq(schema.documents.id, documentId),
162
+ eq(schema.documents.spaceId, spaceId),
163
+ ),
164
+ );
165
+ }
166
+ });
167
+ await writeAppState("refresh-signal", { ts: Date.now() });
168
+ return {
169
+ success: true,
170
+ sourceId,
171
+ disconnectedDocuments,
172
+ localFilesDeleted: 0,
173
+ };
174
+ },
175
+ });
@@ -6,6 +6,7 @@ import { and, eq, gte, isNull, sql } from "drizzle-orm";
6
6
  import { z } from "zod";
7
7
 
8
8
  import { getDb, schema } from "../server/db/index.js";
9
+ import { ensureDocumentFilesMembership } from "./_content-files.js";
9
10
  import { getContentDatabaseResponse } from "./_database-utils.js";
10
11
  import { nanoid } from "./_property-utils.js";
11
12
 
@@ -48,6 +49,12 @@ export default defineAction({
48
49
  );
49
50
 
50
51
  if (!row) throw new Error("Database row not found.");
52
+ if (!row.database.spaceId) {
53
+ throw new Error("Database does not belong to a Content space.");
54
+ }
55
+ if (row.document.spaceId !== row.database.spaceId) {
56
+ throw new Error("Cannot duplicate a database row across Content spaces.");
57
+ }
51
58
 
52
59
  await assertAccess("document", row.database.documentId, "editor");
53
60
  await assertAccess("document", row.document.id, "viewer");
@@ -103,6 +110,7 @@ export default defineAction({
103
110
 
104
111
  await tx.insert(schema.documents).values({
105
112
  id: nextDocumentId,
113
+ spaceId: row.database.spaceId,
106
114
  ownerEmail: row.document.ownerEmail,
107
115
  orgId: row.document.orgId,
108
116
  parentId: row.database.documentId,
@@ -155,6 +163,8 @@ export default defineAction({
155
163
  })),
156
164
  );
157
165
  }
166
+
167
+ await ensureDocumentFilesMembership(tx, nextDocumentId, now);
158
168
  });
159
169
 
160
170
  await writeAppState("refresh-signal", { ts: Date.now() });
@@ -5,6 +5,7 @@ import { assertAccess } from "@agent-native/core/sharing";
5
5
  import { and, eq, gte, inArray, sql } from "drizzle-orm";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
+ import { ensureDocumentsFilesMembership } from "./_content-files.js";
8
9
  import {
9
10
  databaseRowBatchSchema,
10
11
  resolveDatabaseRowsForBatch,
@@ -19,6 +20,12 @@ export default defineAction({
19
20
  run: async (args) => {
20
21
  const db = getDb();
21
22
  const { database, rows } = await resolveDatabaseRowsForBatch(args);
23
+ if (!database.spaceId) {
24
+ throw new Error("Database does not belong to a Content space.");
25
+ }
26
+ if (rows.some((row) => row.document.spaceId !== database.spaceId)) {
27
+ throw new Error("Cannot duplicate database rows across Content spaces.");
28
+ }
22
29
 
23
30
  await assertAccess("document", database.documentId, "editor");
24
31
  for (const row of rows) {
@@ -103,6 +110,7 @@ export default defineAction({
103
110
  await tx.insert(schema.documents).values(
104
111
  duplicates.map((duplicate) => ({
105
112
  id: duplicate.duplicatedDocumentId,
113
+ spaceId: database.spaceId,
106
114
  ownerEmail: duplicate.row.document.ownerEmail,
107
115
  orgId: duplicate.row.document.orgId,
108
116
  parentId: database.documentId,
@@ -163,6 +171,11 @@ export default defineAction({
163
171
  ),
164
172
  );
165
173
  }
174
+ await ensureDocumentsFilesMembership(
175
+ tx,
176
+ duplicates.map((duplicate) => duplicate.duplicatedDocumentId),
177
+ now,
178
+ );
166
179
  });
167
180
 
168
181
  await writeAppState("refresh-signal", { ts: Date.now() });
@@ -10,11 +10,6 @@ import { eq } from "drizzle-orm";
10
10
  import { z } from "zod";
11
11
 
12
12
  import { getDb, schema } from "../server/db/index.js";
13
- import {
14
- getLocalFileDocument,
15
- isContentLocalFileMode,
16
- updateLocalFileDocument,
17
- } from "./_local-file-documents.js";
18
13
 
19
14
  interface TextEdit {
20
15
  find: string;
@@ -72,19 +67,8 @@ export default defineAction({
72
67
  if (edit.replace === undefined) edit.replace = "";
73
68
  }
74
69
 
75
- const localFileMode = await isContentLocalFileMode();
76
- const existing = await (async () => {
77
- if (localFileMode) {
78
- const doc = await getLocalFileDocument(id);
79
- if (doc.source?.kind === "folder") {
80
- throw new Error("Folders cannot be edited directly");
81
- }
82
- return doc;
83
- }
84
-
85
- const access = await assertAccess("document", id, "editor");
86
- return access.resource;
87
- })();
70
+ const access = await assertAccess("document", id, "editor");
71
+ const existing = access.resource;
88
72
 
89
73
  // ─── Apply edits to the document markdown ───────────────────────────────
90
74
  //
@@ -128,16 +112,6 @@ export default defineAction({
128
112
  return { applied: 0, total: edits.length, results };
129
113
  }
130
114
 
131
- if (localFileMode) {
132
- await updateLocalFileDocument(id, { content });
133
- await writeAppState("refresh-signal", { ts: Date.now() });
134
- return {
135
- applied: changeCount,
136
- total: edits.length,
137
- results,
138
- };
139
- }
140
-
141
115
  // Persist. The fresh updatedAt is the signal the open editor uses to tell an
142
116
  // intentional external edit apart from a stale autosave echo.
143
117
  const db = getDb();
@@ -0,0 +1,26 @@
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 { z } from "zod";
5
+
6
+ import { getDb } from "../server/db/index.js";
7
+ import { reconcileContentFilesMemberships } from "./_content-files.js";
8
+ import { provisionContentSpaces } from "./_content-spaces.js";
9
+
10
+ export default defineAction({
11
+ description:
12
+ "Provision and reconcile the signed-in user's personal and organization Content spaces.",
13
+ schema: z.object({}),
14
+ run: async () => {
15
+ const userEmail = getRequestUserEmail();
16
+ if (!userEmail) throw new Error("no authenticated user");
17
+ const db = getDb();
18
+ const result = await provisionContentSpaces(db, userEmail);
19
+ const reconciliation = await reconcileContentFilesMemberships(
20
+ db,
21
+ userEmail,
22
+ );
23
+ await writeAppState("refresh-signal", { ts: Date.now() });
24
+ return { ...result, reconciliation };
25
+ },
26
+ });
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { asc } from "drizzle-orm";
3
+ import { and, asc, eq, inArray } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
@@ -10,13 +10,21 @@ import {
10
10
  } from "../server/lib/documents.js";
11
11
  import {
12
12
  buildContentSourceBundle,
13
+ serializeContentSourceDocument,
13
14
  type ContentSourceDocument,
14
15
  } from "../shared/content-source.js";
15
16
 
16
17
  export default defineAction({
17
18
  description:
18
19
  "Export editable Content documents as source-control friendly Markdown/MDX files with frontmatter.",
19
- schema: z.object({}),
20
+ schema: z.object({
21
+ sourceId: z
22
+ .string()
23
+ .optional()
24
+ .describe(
25
+ "Connected local-folder source to export using its relative paths",
26
+ ),
27
+ }),
20
28
  http: { method: "GET" },
21
29
  readOnly: true,
22
30
  publicAgent: {
@@ -27,11 +35,28 @@ export default defineAction({
27
35
  description:
28
36
  "Export Content documents as a local-file source bundle for MDX workflows.",
29
37
  },
30
- run: async () => {
31
- const rows = await getDb()
38
+ run: async ({ sourceId }) => {
39
+ const db = getDb();
40
+ const sourceRows = sourceId
41
+ ? await db
42
+ .select()
43
+ .from(schema.contentDatabaseSourceRows)
44
+ .where(eq(schema.contentDatabaseSourceRows.sourceId, sourceId))
45
+ : [];
46
+ const sourceDocumentIds = sourceRows.map((row) => row.documentId);
47
+ const rows = await db
32
48
  .select()
33
49
  .from(schema.documents)
34
- .where(accessFilter(schema.documents, schema.documentShares))
50
+ .where(
51
+ sourceId
52
+ ? sourceDocumentIds.length
53
+ ? and(
54
+ accessFilter(schema.documents, schema.documentShares),
55
+ inArray(schema.documents.id, sourceDocumentIds),
56
+ )
57
+ : eq(schema.documents.id, "__no_local_folder_documents__")
58
+ : accessFilter(schema.documents, schema.documentShares),
59
+ )
35
60
  .orderBy(asc(schema.documents.position), asc(schema.documents.title));
36
61
 
37
62
  const documents: ContentSourceDocument[] = rows.map((doc) => ({
@@ -48,9 +73,22 @@ export default defineAction({
48
73
  updatedAt: doc.updatedAt,
49
74
  }));
50
75
 
51
- return {
52
- ...buildContentSourceBundle(documents),
53
- count: documents.length,
54
- };
76
+ if (sourceId) {
77
+ const pathByDocumentId = new Map(
78
+ sourceRows.map((row) => [row.documentId, row.sourceDisplayKey]),
79
+ );
80
+ return {
81
+ root: "",
82
+ exportedAt: new Date().toISOString(),
83
+ files: Object.fromEntries(
84
+ documents.map((document) => [
85
+ pathByDocumentId.get(document.id) ?? `content/${document.id}.mdx`,
86
+ serializeContentSourceDocument(document),
87
+ ]),
88
+ ),
89
+ count: documents.length,
90
+ };
91
+ }
92
+ return { ...buildContentSourceBundle(documents), count: documents.length };
55
93
  },
56
94
  });
@@ -15,12 +15,6 @@ import {
15
15
  serializeDatabaseMembership,
16
16
  } from "./_database-utils.js";
17
17
  import { serializeDocumentSource } from "./_document-source.js";
18
- import {
19
- getLocalFileDocument,
20
- getLocalDocumentContextPath,
21
- isLocalDocumentId,
22
- isContentLocalFileMode,
23
- } from "./_local-file-documents.js";
24
18
  import "../server/db/index.js";
25
19
  import {
26
20
  listPropertiesForDocument,
@@ -46,14 +40,6 @@ export default defineAction({
46
40
  run: async (args) => {
47
41
  if (!args.id) throw new Error("--id is required");
48
42
 
49
- if ((await isContentLocalFileMode()) && isLocalDocumentId(args.id)) {
50
- const document = await getLocalFileDocument(args.id);
51
- return {
52
- ...document,
53
- contextPath: await getLocalDocumentContextPath(args.id),
54
- };
55
- }
56
-
57
43
  const access = await resolveAccess("document", args.id);
58
44
  // Not-found is a deterministic client-state condition (deleted or
59
45
  // inaccessible document still referenced by an open tab) — 404, not a
@@ -15,6 +15,13 @@ import {
15
15
  parseContentSourceFile,
16
16
  type ParsedContentSourceFile,
17
17
  } from "../shared/content-source.js";
18
+ import { ensureDocumentsFilesMembership } from "./_content-files.js";
19
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
20
+ import {
21
+ organizationContentSpaceId,
22
+ personalContentSpaceId,
23
+ provisionContentSpaces,
24
+ } from "./_content-spaces.js";
18
25
 
19
26
  const MAX_SOURCE_FILES = 500;
20
27
  const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024;
@@ -187,6 +194,18 @@ export default defineAction({
187
194
  const currentOrgId = getRequestOrgId() ?? null;
188
195
  const db = getDb();
189
196
  const now = new Date().toISOString();
197
+ const defaultSpaceId = currentOrgId
198
+ ? organizationContentSpaceId(currentOrgId)
199
+ : personalContentSpaceId(currentUserEmail);
200
+ if (!dryRun) {
201
+ const provisioned = await provisionContentSpaces(db, currentUserEmail);
202
+ if (!provisioned.spaceIds.includes(defaultSpaceId)) {
203
+ throw new Error(
204
+ "The active organization does not have a writable Content space.",
205
+ );
206
+ }
207
+ await resolveContentSpaceAccess(defaultSpaceId, "editor");
208
+ }
190
209
  const parsed: ParsedContentSourceFile[] = entries.map(
191
210
  ([filePath, source]) => parseContentSourceFile(filePath, source),
192
211
  );
@@ -229,6 +248,7 @@ export default defineAction({
229
248
  const idByPath = new Map<string, string>();
230
249
  const pathById = new Map<string, string>();
231
250
  const ownerById = new Map<string, string>();
251
+ const spaceById = new Map<string, string>();
232
252
  const desiredParentById = new Map<string, string | null>();
233
253
  const desiredPositionById = new Map<string, number>();
234
254
  const currentParentById = new Map<string, string | null>();
@@ -271,7 +291,9 @@ export default defineAction({
271
291
  }
272
292
 
273
293
  if (existing) {
294
+ const existingSpaceId = existing.spaceId ?? defaultSpaceId;
274
295
  ownerById.set(id, existing.ownerEmail as string);
296
+ spaceById.set(id, existingSpaceId);
275
297
  currentParentById.set(id, existing.parentId ?? null);
276
298
  currentPositionById.set(id, existing.position ?? 0);
277
299
  if (file.parentId !== undefined) {
@@ -299,6 +321,7 @@ export default defineAction({
299
321
  existing.sourceKind !== sourceUpdates.sourceKind ||
300
322
  existing.sourcePath !== sourceUpdates.sourcePath ||
301
323
  existing.sourceRootPath !== sourceUpdates.sourceRootPath;
324
+ const spaceChanged = !existing.spaceId;
302
325
  const anyChange =
303
326
  titleChanged ||
304
327
  contentChanged ||
@@ -306,7 +329,8 @@ export default defineAction({
306
329
  iconChanged ||
307
330
  favoriteChanged ||
308
331
  discoverabilityChanged ||
309
- sourceChanged;
332
+ sourceChanged ||
333
+ spaceChanged;
310
334
 
311
335
  if (!anyChange) {
312
336
  unchanged.push({ id, path: file.path, title: existing.title });
@@ -324,6 +348,7 @@ export default defineAction({
324
348
  }
325
349
 
326
350
  const updates: Record<string, unknown> = { updatedAt: now };
351
+ if (!existing.spaceId) updates.spaceId = existingSpaceId;
327
352
  if (titleChanged) updates.title = file.title;
328
353
  if (contentChanged) updates.content = file.content;
329
354
  if (descriptionChanged) updates.description = file.description;
@@ -348,6 +373,7 @@ export default defineAction({
348
373
  try {
349
374
  await db.insert(schema.documents).values({
350
375
  id,
376
+ spaceId: defaultSpaceId,
351
377
  ownerEmail: currentUserEmail,
352
378
  orgId: currentOrgId,
353
379
  parentId: null,
@@ -382,6 +408,7 @@ export default defineAction({
382
408
  desiredParentById.set(id, file.parentId ?? null);
383
409
  desiredPositionById.set(id, file.position ?? index);
384
410
  ownerById.set(id, currentUserEmail);
411
+ spaceById.set(id, defaultSpaceId);
385
412
  created.push({ id, path: file.path, title: file.title });
386
413
  }
387
414
 
@@ -410,7 +437,8 @@ export default defineAction({
410
437
  const parentAccess = await resolveAccess("document", parentId);
411
438
  if (
412
439
  parentAccess &&
413
- (parentAccess.resource.ownerEmail as string) === ownerEmail
440
+ (parentAccess.resource.ownerEmail as string) === ownerEmail &&
441
+ parentAccess.resource.spaceId === spaceById.get(id)
414
442
  ) {
415
443
  try {
416
444
  await assertParentIsNotDescendant({ ownerEmail, id, parentId });
@@ -428,7 +456,7 @@ export default defineAction({
428
456
  } else {
429
457
  skipped.push({
430
458
  path: pathById.get(id) ?? id,
431
- reason: `Skipped parent "${parentId}" because it is not editable in the same owner scope.`,
459
+ reason: `Skipped parent "${parentId}" because it is not editable in the same Content space.`,
432
460
  });
433
461
  }
434
462
  }
@@ -452,6 +480,18 @@ export default defineAction({
452
480
  .where(eq(schema.documents.id, id));
453
481
  }
454
482
 
483
+ await ensureDocumentsFilesMembership(
484
+ db,
485
+ [
486
+ ...new Set([
487
+ ...created.map((row) => row.id),
488
+ ...updated.map((row) => row.id),
489
+ ...unchanged.map((row) => row.id),
490
+ ]),
491
+ ],
492
+ now,
493
+ );
494
+
455
495
  await writeAppState("refresh-signal", { ts: Date.now() });
456
496
  }
457
497
 
@@ -0,0 +1,103 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
+ import { and, eq, isNull } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import {
8
+ listContentOrganizationMemberships,
9
+ normalizeContentSpaceEmail,
10
+ } from "./_content-space-access.js";
11
+ import {
12
+ personalContentSpaceId,
13
+ systemIdsForContentSpace,
14
+ } from "./_content-spaces.js";
15
+
16
+ export default defineAction({
17
+ description:
18
+ "List Content spaces that are already provisioned and currently authorized for the signed-in user.",
19
+ schema: z.object({}),
20
+ http: { method: "GET" },
21
+ readOnly: true,
22
+ run: async () => {
23
+ const userEmail = getRequestUserEmail();
24
+ if (!userEmail) throw new Error("no authenticated user");
25
+ const email = normalizeContentSpaceEmail(userEmail);
26
+ const db = getDb();
27
+ const personalSpaceId = personalContentSpaceId(email);
28
+ const catalogIds = systemIdsForContentSpace(personalSpaceId, "workspaces");
29
+ const memberships = await listContentOrganizationMemberships(email);
30
+ const roleByOrgId = new Map(
31
+ memberships.map((membership) => [
32
+ membership.orgId,
33
+ membership.role === "owner"
34
+ ? "owner"
35
+ : membership.role === "admin"
36
+ ? "editor"
37
+ : "viewer",
38
+ ]),
39
+ );
40
+ const rows = await db
41
+ .select({
42
+ mapping: schema.contentSpaceCatalogItems,
43
+ space: schema.contentSpaces,
44
+ item: schema.contentDatabaseItems,
45
+ })
46
+ .from(schema.contentSpaceCatalogItems)
47
+ .innerJoin(
48
+ schema.contentSpaces,
49
+ eq(schema.contentSpaces.id, schema.contentSpaceCatalogItems.spaceId),
50
+ )
51
+ .innerJoin(
52
+ schema.contentDatabaseItems,
53
+ eq(
54
+ schema.contentDatabaseItems.id,
55
+ schema.contentSpaceCatalogItems.databaseItemId,
56
+ ),
57
+ )
58
+ .where(
59
+ and(
60
+ eq(schema.contentSpaceCatalogItems.ownerEmail, email),
61
+ eq(
62
+ schema.contentSpaceCatalogItems.catalogDatabaseId,
63
+ catalogIds.databaseId,
64
+ ),
65
+ isNull(schema.contentSpaces.archivedAt),
66
+ ),
67
+ );
68
+ const spaces = [] as Array<{
69
+ id: string;
70
+ name: string;
71
+ kind: string;
72
+ filesDatabaseId: string;
73
+ orgId: string | null;
74
+ role: string;
75
+ catalogItemId: string;
76
+ catalogDocumentId: string;
77
+ }>;
78
+ for (const row of rows) {
79
+ if (
80
+ row.item.databaseId !== catalogIds.databaseId ||
81
+ row.item.documentId !== row.mapping.documentId
82
+ )
83
+ continue;
84
+ const role = row.space.orgId
85
+ ? roleByOrgId.get(row.space.orgId)
86
+ : row.space.ownerEmail.toLowerCase() === email
87
+ ? "owner"
88
+ : undefined;
89
+ if (!role) continue;
90
+ spaces.push({
91
+ id: row.space.id,
92
+ name: row.space.name,
93
+ kind: row.space.kind,
94
+ filesDatabaseId: row.space.filesDatabaseId,
95
+ orgId: row.space.orgId,
96
+ role,
97
+ catalogItemId: row.mapping.databaseItemId,
98
+ catalogDocumentId: row.mapping.documentId,
99
+ });
100
+ }
101
+ return { catalogDatabaseId: catalogIds.databaseId, spaces };
102
+ },
103
+ });