@agent-native/core 0.102.2 → 0.103.1

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 (205) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -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/core/src/vite/client.ts +2 -0
  32. package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
  33. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
  34. package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
  35. package/corpus/templates/assets/app/i18n-data.ts +215 -0
  36. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
  37. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
  38. package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -0
  39. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +12 -2
  40. package/corpus/templates/clips/app/components/player/settings-panel.tsx +30 -12
  41. package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
  42. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
  43. package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -2
  44. package/corpus/templates/clips/app/i18n/de-DE.ts +4 -2
  45. package/corpus/templates/clips/app/i18n/en-US.ts +3 -2
  46. package/corpus/templates/clips/app/i18n/es-ES.ts +4 -2
  47. package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -2
  48. package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -2
  49. package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -1
  50. package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -2
  51. package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -2
  52. package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -2
  53. package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -2
  54. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +27 -1
  55. package/corpus/templates/clips/server/lib/share-password.ts +6 -3
  56. package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
  57. package/corpus/templates/content/AGENTS.md +25 -30
  58. package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
  59. package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
  60. package/corpus/templates/content/actions/_content-files.ts +338 -0
  61. package/corpus/templates/content/actions/_content-space-access.ts +135 -0
  62. package/corpus/templates/content/actions/_content-spaces.ts +640 -0
  63. package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
  64. package/corpus/templates/content/actions/_database-utils.ts +7 -1
  65. package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
  66. package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
  67. package/corpus/templates/content/actions/_property-utils.ts +9 -4
  68. package/corpus/templates/content/actions/add-database-item.ts +108 -65
  69. package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
  70. package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
  71. package/corpus/templates/content/actions/create-content-database.ts +129 -3
  72. package/corpus/templates/content/actions/create-document.ts +65 -55
  73. package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
  74. package/corpus/templates/content/actions/delete-content-database.ts +3 -0
  75. package/corpus/templates/content/actions/delete-document.ts +7 -11
  76. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
  77. package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
  78. package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
  79. package/corpus/templates/content/actions/edit-document.ts +2 -28
  80. package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
  81. package/corpus/templates/content/actions/export-content-source.ts +47 -9
  82. package/corpus/templates/content/actions/get-document.ts +0 -14
  83. package/corpus/templates/content/actions/import-content-source.ts +43 -3
  84. package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
  85. package/corpus/templates/content/actions/list-documents.ts +12 -9
  86. package/corpus/templates/content/actions/move-document.ts +3 -14
  87. package/corpus/templates/content/actions/pull-document.ts +0 -23
  88. package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
  89. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
  90. package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
  91. package/corpus/templates/content/actions/search-documents.ts +0 -34
  92. package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
  93. package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
  94. package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
  95. package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
  96. package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
  97. package/corpus/templates/content/actions/update-document.ts +0 -15
  98. package/corpus/templates/content/actions/view-screen.ts +2 -50
  99. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
  100. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
  101. package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
  102. package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
  103. package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
  104. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
  105. package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
  106. package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
  107. package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
  108. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
  109. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
  110. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
  111. package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
  112. package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
  113. package/corpus/templates/content/app/i18n-data.ts +75 -0
  114. package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
  115. package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
  116. package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
  117. package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
  118. package/corpus/templates/content/parity/matrix.md +2 -1
  119. package/corpus/templates/content/parity/matrix.ts +37 -1
  120. package/corpus/templates/content/scripts/dev-database.mjs +7 -5
  121. package/corpus/templates/content/server/db/schema.ts +113 -40
  122. package/corpus/templates/content/server/lib/document-context.ts +15 -1
  123. package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
  124. package/corpus/templates/content/server/plugins/db.ts +153 -0
  125. package/corpus/templates/content/shared/api.ts +17 -3
  126. package/corpus/templates/content/shared/builder-mdx.ts +163 -16
  127. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
  128. package/corpus/templates/design/AGENTS.md +6 -1
  129. package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
  130. package/corpus/templates/design/actions/list-design-templates.ts +69 -21
  131. package/corpus/templates/design/actions/navigate.ts +2 -2
  132. package/corpus/templates/design/actions/view-screen.ts +15 -2
  133. package/corpus/templates/design/agent-native.app-skill.json +5 -0
  134. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
  135. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
  136. package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
  137. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
  138. package/corpus/templates/design/app/i18n-data.ts +21 -2
  139. package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
  140. package/corpus/templates/design/app/pages/Index.tsx +195 -24
  141. package/corpus/templates/design/app/pages/Templates.tsx +144 -55
  142. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
  143. package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
  144. package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
  145. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  146. package/corpus/templates/mail/AGENTS.md +23 -18
  147. package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
  148. package/corpus/templates/mail/actions/mark-read.ts +131 -20
  149. package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
  150. package/corpus/templates/mail/actions/move-email.ts +30 -24
  151. package/corpus/templates/mail/actions/send-email.ts +73 -68
  152. package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
  153. package/corpus/templates/mail/server/handlers/emails.ts +218 -209
  154. package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
  155. package/corpus/templates/mail/server/lib/email-state.ts +198 -113
  156. package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
  157. package/corpus/templates/mail/server/lib/jobs.ts +69 -74
  158. package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
  159. package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
  160. package/dist/cli/content-local.d.ts +1 -0
  161. package/dist/cli/content-local.d.ts.map +1 -1
  162. package/dist/cli/content-local.js +75 -17
  163. package/dist/cli/content-local.js.map +1 -1
  164. package/dist/cli/index.js +2 -2
  165. package/dist/cli/index.js.map +1 -1
  166. package/dist/cli/skills-content/content-skill.d.ts +1 -1
  167. package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
  168. package/dist/cli/skills-content/content-skill.js +36 -25
  169. package/dist/cli/skills-content/content-skill.js.map +1 -1
  170. package/dist/cli/skills.d.ts.map +1 -1
  171. package/dist/cli/skills.js +47 -14
  172. package/dist/cli/skills.js.map +1 -1
  173. package/dist/collab/struct-routes.d.ts +1 -1
  174. package/dist/local-artifacts/index.d.ts +10 -0
  175. package/dist/local-artifacts/index.d.ts.map +1 -1
  176. package/dist/local-artifacts/index.js +39 -0
  177. package/dist/local-artifacts/index.js.map +1 -1
  178. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  179. package/dist/vite/client.d.ts.map +1 -1
  180. package/dist/vite/client.js +2 -0
  181. package/dist/vite/client.js.map +1 -1
  182. package/docs/content/local-file-mode.mdx +99 -489
  183. package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  184. package/docs/content/locales/ar-SA/template-content.mdx +10 -24
  185. package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  186. package/docs/content/locales/de-DE/template-content.mdx +10 -24
  187. package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  188. package/docs/content/locales/es-ES/template-content.mdx +10 -24
  189. package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  190. package/docs/content/locales/fr-FR/template-content.mdx +10 -24
  191. package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  192. package/docs/content/locales/hi-IN/template-content.mdx +10 -24
  193. package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  194. package/docs/content/locales/ja-JP/template-content.mdx +10 -24
  195. package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  196. package/docs/content/locales/ko-KR/template-content.mdx +10 -24
  197. package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  198. package/docs/content/locales/pt-BR/template-content.mdx +10 -24
  199. package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  200. package/docs/content/locales/zh-CN/template-content.mdx +9 -24
  201. package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  202. package/docs/content/locales/zh-TW/template-content.mdx +9 -24
  203. package/docs/content/template-content.mdx +26 -42
  204. package/package.json +1 -1
  205. package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
@@ -19,10 +19,6 @@ import {
19
19
  } from "../server/lib/documents.js";
20
20
  import { serializeDatabaseMembership } from "./_database-utils.js";
21
21
  import { serializeDocumentSource } from "./_document-source.js";
22
- import {
23
- isContentLocalFileMode,
24
- listLocalFileDocuments,
25
- } from "./_local-file-documents.js";
26
22
  import { parseDatabaseViewConfig } from "./_property-utils.js";
27
23
 
28
24
  function contentPreview(content: string, maxLength = 180) {
@@ -52,9 +48,6 @@ export default defineAction({
52
48
  schema: z.object({}),
53
49
  http: { method: "GET" },
54
50
  run: async () => {
55
- const localFileMode = await isContentLocalFileMode();
56
- const localDocuments = localFileMode ? await listLocalFileDocuments() : [];
57
-
58
51
  const db = getDb();
59
52
  const userEmail = getRequestUserEmail();
60
53
  const orgId = getRequestOrgId();
@@ -174,6 +167,10 @@ export default defineAction({
174
167
  inArray(schema.contentDatabases.documentId, visibleDocumentIds),
175
168
  isNull(schema.contentDatabases.deletedAt),
176
169
  ),
170
+ )
171
+ .orderBy(
172
+ sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
173
+ asc(schema.contentDatabases.id),
177
174
  ),
178
175
  db
179
176
  .select({
@@ -196,6 +193,10 @@ export default defineAction({
196
193
  ),
197
194
  isNull(schema.contentDatabases.deletedAt),
198
195
  ),
196
+ )
197
+ .orderBy(
198
+ sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
199
+ asc(schema.contentDatabases.id),
199
200
  ),
200
201
  db
201
202
  .select({
@@ -230,7 +231,9 @@ export default defineAction({
230
231
  }
231
232
 
232
233
  for (const row of databaseMemberships) {
233
- databaseMembershipByDocumentId.set(row.item.documentId, row);
234
+ if (!databaseMembershipByDocumentId.has(row.item.documentId)) {
235
+ databaseMembershipByDocumentId.set(row.item.documentId, row);
236
+ }
234
237
  }
235
238
 
236
239
  for (const database of softDeletedDatabases) {
@@ -318,6 +321,6 @@ export default defineAction({
318
321
  };
319
322
  });
320
323
 
321
- return { documents: [...localDocuments, ...mapped] };
324
+ return { documents: mapped };
322
325
  },
323
326
  });
@@ -9,11 +9,6 @@ import {
9
9
  parseDocumentFavorite,
10
10
  parseDocumentHideFromSearch,
11
11
  } from "../server/lib/documents.js";
12
- import {
13
- isLocalDocumentId,
14
- isContentLocalFileMode,
15
- moveLocalFileDocument,
16
- } from "./_local-file-documents.js";
17
12
  import { documentsPositionScope, withPositionLock } from "./_position-utils.js";
18
13
 
19
14
  async function assertParentIsNotDescendant({
@@ -214,15 +209,6 @@ export default defineAction({
214
209
  throw new Error("A document cannot be moved under itself");
215
210
  }
216
211
 
217
- if ((await isContentLocalFileMode()) && isLocalDocumentId(id)) {
218
- const doc = await moveLocalFileDocument(id, args);
219
- await writeAppState("refresh-signal", { ts: Date.now() });
220
- return {
221
- ...doc,
222
- urlPath: `/page/${doc.id}`,
223
- };
224
- }
225
-
226
212
  const access = await assertAccess("document", id, "editor");
227
213
  const existing = access.resource;
228
214
  const ownerEmail = existing.ownerEmail as string;
@@ -243,6 +229,9 @@ export default defineAction({
243
229
  if (parentAccess.resource.ownerEmail !== ownerEmail) {
244
230
  throw new Error("Parent document must belong to the same owner");
245
231
  }
232
+ if (parentAccess.resource.spaceId !== existing.spaceId) {
233
+ throw new Error("Parent document must be in the same Content space");
234
+ }
246
235
  if (!sameRootSection(parentAccess.resource, existing)) {
247
236
  throw new Error("Parent document must be in the same section");
248
237
  }
@@ -5,10 +5,6 @@ import { z } from "zod";
5
5
 
6
6
  import "../server/db/index.js";
7
7
  import { flushOpenDocumentEditorToSql } from "./_document-flush.js";
8
- import {
9
- getLocalFileDocument,
10
- isContentLocalFileMode,
11
- } from "./_local-file-documents.js";
12
8
 
13
9
  /**
14
10
  * Collab-aware "ingest the final" read for external agents.
@@ -65,25 +61,6 @@ export default defineAction({
65
61
  readOnly: true,
66
62
  publicAgent: { expose: true, readOnly: true, requiresAuth: true },
67
63
  run: async ({ id, format }) => {
68
- if (await isContentLocalFileMode()) {
69
- const doc = await getLocalFileDocument(id);
70
- if (doc.source?.kind === "folder") {
71
- throw new Error("Folders cannot be pulled as markdown documents");
72
- }
73
- return {
74
- id: doc.id,
75
- title: doc.title,
76
- description: doc.description ?? "",
77
- content: formatDocumentContent(doc.content ?? "", format),
78
- format,
79
- deepLink: buildDeepLink({
80
- app: "content",
81
- view: "editor",
82
- params: { documentId: doc.id },
83
- }),
84
- };
85
- }
86
-
87
64
  const access = await resolveAccess("document", id);
88
65
  if (!access) throw new Error(`Document "${id}" not found`);
89
66
 
@@ -1,18 +1,11 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
4
- import { and, eq } from "drizzle-orm";
4
+ import { and, eq, inArray } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
- import {
9
- invalidateLocalFileDocumentsCache,
10
- isContentLocalFileMode,
11
- removeContentLocalFileRoots,
12
- } from "./_local-file-documents.js";
13
- import { deleteDocumentRecursive } from "./delete-document.js";
14
-
15
- async function removeImportedLocalSourceDocuments(
8
+ async function unlinkImportedLocalSourceDocuments(
16
9
  sourceRootPath?: string | null,
17
10
  ) {
18
11
  const db = getDb();
@@ -33,29 +26,60 @@ async function removeImportedLocalSourceDocuments(
33
26
  .where(
34
27
  and(...clauses, accessFilter(schema.documents, schema.documentShares)),
35
28
  );
36
- const candidateIds = new Set(candidates.map((document) => document.id));
37
- const roots = candidates.filter(
38
- (document) => !document.parentId || !candidateIds.has(document.parentId),
39
- );
40
- let deleted = 0;
29
+ const trackedRows = candidates.length
30
+ ? await db
31
+ .select({ documentId: schema.contentDatabaseSourceRows.documentId })
32
+ .from(schema.contentDatabaseSourceRows)
33
+ .innerJoin(
34
+ schema.contentDatabaseSources,
35
+ eq(
36
+ schema.contentDatabaseSources.id,
37
+ schema.contentDatabaseSourceRows.sourceId,
38
+ ),
39
+ )
40
+ .where(
41
+ and(
42
+ inArray(
43
+ schema.contentDatabaseSourceRows.documentId,
44
+ candidates.map((document) => document.id),
45
+ ),
46
+ eq(schema.contentDatabaseSources.sourceType, "local-folder"),
47
+ ),
48
+ )
49
+ : [];
50
+ const trackedDocumentIds = new Set(trackedRows.map((row) => row.documentId));
51
+ const documentIds: string[] = [];
52
+ for (const document of candidates) {
53
+ if (trackedDocumentIds.has(document.id)) continue;
54
+ await assertAccess("document", document.id, "admin");
55
+ documentIds.push(document.id);
56
+ }
41
57
 
42
- for (const document of roots) {
43
- const access = await assertAccess("document", document.id, "admin");
44
- deleted += (
45
- await deleteDocumentRecursive(
46
- db,
47
- document.id,
48
- access.resource.ownerEmail as string,
49
- )
50
- ).length;
58
+ if (documentIds.length > 0) {
59
+ const now = new Date().toISOString();
60
+ await db
61
+ .update(schema.documents)
62
+ .set({
63
+ sourceMode: "database",
64
+ sourceKind: null,
65
+ sourcePath: null,
66
+ sourceRootPath: null,
67
+ sourceUpdatedAt: now,
68
+ updatedAt: now,
69
+ })
70
+ .where(inArray(schema.documents.id, documentIds));
51
71
  }
52
72
 
53
- return { removed: deleted, roots: [] as string[], manifestPath: null };
73
+ return {
74
+ removed: documentIds.length,
75
+ roots: [] as string[],
76
+ manifestPath: null,
77
+ };
54
78
  }
55
79
 
56
80
  export default defineAction({
57
81
  description:
58
- "Remove local-file sources from Content without deleting the files on disk. In local-file mode this unlinks configured Content roots from agent-native.json; in database mode this removes imported local-file document entries.",
82
+ "Unlink legacy local-file source metadata from SQL-backed Content pages without deleting either the pages or files on disk. Use disconnect-local-folder-source for connected folder adapters.",
59
83
  schema: z.object({
60
84
  sourceRootPath: z
61
85
  .string()
@@ -75,9 +99,7 @@ export default defineAction({
75
99
  "Unlink local-file entries from Content without deleting local Markdown or MDX files.",
76
100
  },
77
101
  run: async ({ sourceRootPath }) => {
78
- const result = (await isContentLocalFileMode())
79
- ? await removeContentLocalFileRoots(sourceRootPath)
80
- : await removeImportedLocalSourceDocuments(sourceRootPath);
102
+ const result = await unlinkImportedLocalSourceDocuments(sourceRootPath);
81
103
 
82
104
  if (result.removed === 0 && result.roots.length === 0) {
83
105
  throw new Error(
@@ -87,7 +109,6 @@ export default defineAction({
87
109
  );
88
110
  }
89
111
 
90
- invalidateLocalFileDocumentsCache();
91
112
  await writeAppState("refresh-signal", { ts: Date.now() });
92
113
 
93
114
  return {
@@ -0,0 +1,442 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { defineAction } from "@agent-native/core";
4
+ import { writeAppState } from "@agent-native/core/application-state";
5
+ import { assertAccess } from "@agent-native/core/sharing";
6
+ import { and, eq, ne } from "drizzle-orm";
7
+ import { z } from "zod";
8
+
9
+ import { getDb, schema } from "../server/db/index.js";
10
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
11
+ import { LOCAL_FOLDER_SOURCE_TYPE } from "./_local-folder-source.js";
12
+
13
+ const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024;
14
+
15
+ function hash(content: string) {
16
+ return createHash("sha256").update(content).digest("hex");
17
+ }
18
+
19
+ function metadataHash(value: {
20
+ title: string;
21
+ description?: string | null;
22
+ icon?: string | null;
23
+ }) {
24
+ return hash(
25
+ JSON.stringify({
26
+ title: value.title,
27
+ description: value.description ?? "",
28
+ icon: value.icon ?? null,
29
+ }),
30
+ );
31
+ }
32
+
33
+ function parseObject(value: string | null | undefined) {
34
+ if (!value) return {} as Record<string, unknown>;
35
+ try {
36
+ const parsed = JSON.parse(value) as unknown;
37
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
38
+ ? (parsed as Record<string, unknown>)
39
+ : {};
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ function proposedMetadata(value: string | null | undefined) {
46
+ if (!value) return {} as Record<string, string | null>;
47
+ try {
48
+ const parsed = JSON.parse(value) as unknown;
49
+ if (!Array.isArray(parsed)) return {};
50
+ return Object.fromEntries(
51
+ parsed.flatMap((change) => {
52
+ if (
53
+ !change ||
54
+ typeof change !== "object" ||
55
+ !("field" in change) ||
56
+ !("proposedValue" in change) ||
57
+ !["title", "description", "icon"].includes(String(change.field))
58
+ ) {
59
+ return [];
60
+ }
61
+ const proposed = change.proposedValue;
62
+ return typeof proposed === "string" || proposed === null
63
+ ? [[String(change.field), proposed] as const]
64
+ : [];
65
+ }),
66
+ );
67
+ } catch {
68
+ return {};
69
+ }
70
+ }
71
+
72
+ export default defineAction({
73
+ description:
74
+ "Resolve an incoming local-folder conflict by keeping Content or accepting the exact reviewed folder revision supplied by the trusted bridge.",
75
+ schema: z.object({
76
+ changeSetId: z.string().min(1),
77
+ decision: z.enum(["keep_content", "accept_source"]),
78
+ sourceContent: z.string().max(MAX_SOURCE_FILE_BYTES).optional(),
79
+ }),
80
+ run: async ({ changeSetId, decision, sourceContent }) => {
81
+ const db = getDb();
82
+ const [target] = await db
83
+ .select({
84
+ changeSet: schema.contentDatabaseSourceChangeSets,
85
+ source: schema.contentDatabaseSources,
86
+ database: schema.contentDatabases,
87
+ document: schema.documents,
88
+ })
89
+ .from(schema.contentDatabaseSourceChangeSets)
90
+ .innerJoin(
91
+ schema.contentDatabaseSources,
92
+ eq(
93
+ schema.contentDatabaseSources.id,
94
+ schema.contentDatabaseSourceChangeSets.sourceId,
95
+ ),
96
+ )
97
+ .innerJoin(
98
+ schema.contentDatabases,
99
+ eq(
100
+ schema.contentDatabases.id,
101
+ schema.contentDatabaseSources.databaseId,
102
+ ),
103
+ )
104
+ .innerJoin(
105
+ schema.documents,
106
+ eq(
107
+ schema.documents.id,
108
+ schema.contentDatabaseSourceChangeSets.documentId,
109
+ ),
110
+ )
111
+ .where(eq(schema.contentDatabaseSourceChangeSets.id, changeSetId));
112
+ if (
113
+ !target ||
114
+ target.source.sourceType !== LOCAL_FOLDER_SOURCE_TYPE ||
115
+ target.changeSet.direction !== "incoming" ||
116
+ target.changeSet.state !== "proposed" ||
117
+ !target.database.spaceId ||
118
+ target.database.systemRole !== "files" ||
119
+ target.database.deletedAt ||
120
+ target.document.spaceId !== target.database.spaceId
121
+ ) {
122
+ throw new Error(`Open local-folder conflict "${changeSetId}" not found`);
123
+ }
124
+ const targetSpaceId = target.database.spaceId;
125
+ await resolveContentSpaceAccess(targetSpaceId, "editor");
126
+ await assertAccess("document", target.document.id, "editor");
127
+ const bodyChange = parseObject(target.changeSet.bodyChangeJson);
128
+ const sourceDeletion = bodyChange.operation === "source_delete";
129
+ const proposedHash =
130
+ typeof bodyChange.proposedHash === "string"
131
+ ? bodyChange.proposedHash
132
+ : null;
133
+ const reviewedContentHash =
134
+ typeof bodyChange.currentHash === "string"
135
+ ? bodyChange.currentHash
136
+ : null;
137
+ const reviewedMetadataHash =
138
+ typeof bodyChange.currentMetadataHash === "string"
139
+ ? bodyChange.currentMetadataHash
140
+ : null;
141
+ if (!sourceDeletion && !proposedHash)
142
+ throw new Error("Conflict is missing its source hash");
143
+ if (
144
+ decision === "accept_source" &&
145
+ !sourceDeletion &&
146
+ sourceContent === undefined
147
+ ) {
148
+ throw new Error(
149
+ "sourceContent is required when accepting the folder revision",
150
+ );
151
+ }
152
+ if (
153
+ decision === "accept_source" &&
154
+ !sourceDeletion &&
155
+ hash(sourceContent!) !== proposedHash
156
+ ) {
157
+ throw new Error(
158
+ "The supplied folder revision changed after review; refresh before resolving",
159
+ );
160
+ }
161
+ if (
162
+ decision === "accept_source" &&
163
+ !sourceDeletion &&
164
+ (!reviewedContentHash || !reviewedMetadataHash)
165
+ ) {
166
+ throw new Error(
167
+ "Conflict is missing its reviewed Content revision; refresh before resolving",
168
+ );
169
+ }
170
+
171
+ const now = new Date().toISOString();
172
+ await db.transaction(async (tx: any) => {
173
+ const claimedChangeSets = await tx
174
+ .update(schema.contentDatabaseSourceChangeSets)
175
+ .set({
176
+ state: decision === "accept_source" ? "applied" : "rejected",
177
+ updatedAt: now,
178
+ })
179
+ .where(
180
+ and(
181
+ eq(schema.contentDatabaseSourceChangeSets.id, changeSetId),
182
+ eq(
183
+ schema.contentDatabaseSourceChangeSets.sourceId,
184
+ target.source.id,
185
+ ),
186
+ eq(schema.contentDatabaseSourceChangeSets.state, "proposed"),
187
+ ),
188
+ )
189
+ .returning({ id: schema.contentDatabaseSourceChangeSets.id });
190
+ if (claimedChangeSets.length !== 1) {
191
+ throw new Error(
192
+ `Open local-folder conflict "${changeSetId}" changed before resolution`,
193
+ );
194
+ }
195
+ const claimedSources = await tx
196
+ .update(schema.contentDatabaseSources)
197
+ .set({ updatedAt: now })
198
+ .where(
199
+ and(
200
+ eq(schema.contentDatabaseSources.id, target.source.id),
201
+ eq(schema.contentDatabaseSources.databaseId, target.database.id),
202
+ eq(
203
+ schema.contentDatabaseSources.sourceType,
204
+ LOCAL_FOLDER_SOURCE_TYPE,
205
+ ),
206
+ ),
207
+ )
208
+ .returning({ id: schema.contentDatabaseSources.id });
209
+ if (claimedSources.length !== 1) {
210
+ throw new Error(
211
+ `Local folder source "${target.source.id}" was disconnected before resolution`,
212
+ );
213
+ }
214
+ if (decision === "accept_source" && sourceDeletion) {
215
+ await tx
216
+ .delete(schema.contentDatabaseSourceRows)
217
+ .where(
218
+ and(
219
+ eq(schema.contentDatabaseSourceRows.sourceId, target.source.id),
220
+ eq(
221
+ schema.contentDatabaseSourceRows.documentId,
222
+ target.document.id,
223
+ ),
224
+ ),
225
+ );
226
+ const [remainingLocalRow] = await tx
227
+ .select({
228
+ sourceDisplayKey: schema.contentDatabaseSourceRows.sourceDisplayKey,
229
+ sourceValuesJson: schema.contentDatabaseSourceRows.sourceValuesJson,
230
+ sourceName: schema.contentDatabaseSources.sourceName,
231
+ })
232
+ .from(schema.contentDatabaseSourceRows)
233
+ .innerJoin(
234
+ schema.contentDatabaseSources,
235
+ eq(
236
+ schema.contentDatabaseSources.id,
237
+ schema.contentDatabaseSourceRows.sourceId,
238
+ ),
239
+ )
240
+ .where(
241
+ and(
242
+ eq(
243
+ schema.contentDatabaseSourceRows.documentId,
244
+ target.document.id,
245
+ ),
246
+ ne(schema.contentDatabaseSourceRows.sourceId, target.source.id),
247
+ eq(
248
+ schema.contentDatabaseSources.sourceType,
249
+ LOCAL_FOLDER_SOURCE_TYPE,
250
+ ),
251
+ ),
252
+ )
253
+ .orderBy(schema.contentDatabaseSources.id)
254
+ .limit(1);
255
+ const remainingValues = parseObject(
256
+ remainingLocalRow?.sourceValuesJson,
257
+ );
258
+ const remainingPath =
259
+ typeof remainingValues.relativePath === "string"
260
+ ? remainingValues.relativePath
261
+ : remainingLocalRow?.sourceDisplayKey;
262
+ await tx
263
+ .update(schema.documents)
264
+ .set(
265
+ remainingLocalRow
266
+ ? {
267
+ sourceMode: "local-files",
268
+ sourceKind: "file",
269
+ sourcePath: remainingPath ?? null,
270
+ sourceRootPath: remainingLocalRow.sourceName,
271
+ sourceUpdatedAt: now,
272
+ updatedAt: now,
273
+ }
274
+ : {
275
+ sourceMode: null,
276
+ sourceKind: null,
277
+ sourcePath: null,
278
+ sourceRootPath: null,
279
+ sourceUpdatedAt: null,
280
+ updatedAt: now,
281
+ },
282
+ )
283
+ .where(
284
+ and(
285
+ eq(schema.documents.id, target.document.id),
286
+ eq(schema.documents.spaceId, targetSpaceId),
287
+ ),
288
+ );
289
+ } else if (decision === "accept_source") {
290
+ const [currentDocument] = await tx
291
+ .select()
292
+ .from(schema.documents)
293
+ .where(
294
+ and(
295
+ eq(schema.documents.id, target.document.id),
296
+ eq(schema.documents.spaceId, targetSpaceId),
297
+ ),
298
+ );
299
+ if (
300
+ !currentDocument ||
301
+ hash(currentDocument.content) !== reviewedContentHash ||
302
+ metadataHash(currentDocument) !== reviewedMetadataHash
303
+ ) {
304
+ throw new Error(
305
+ "Content changed after this conflict was reviewed; refresh before resolving",
306
+ );
307
+ }
308
+ const metadata = proposedMetadata(target.changeSet.fieldChangesJson);
309
+ const resolvedTitle = Object.prototype.hasOwnProperty.call(
310
+ metadata,
311
+ "title",
312
+ )
313
+ ? (metadata.title ?? "")
314
+ : currentDocument.title;
315
+ const resolvedDescription = Object.prototype.hasOwnProperty.call(
316
+ metadata,
317
+ "description",
318
+ )
319
+ ? (metadata.description ?? "")
320
+ : (currentDocument.description ?? "");
321
+ const resolvedIcon = Object.prototype.hasOwnProperty.call(
322
+ metadata,
323
+ "icon",
324
+ )
325
+ ? (metadata.icon ?? null)
326
+ : (currentDocument.icon ?? null);
327
+ await tx
328
+ .insert(schema.documentVersions)
329
+ .values({
330
+ id: `content_document_version_${createHash("sha256")
331
+ .update(
332
+ `${currentDocument.id}:${currentDocument.updatedAt}:${proposedHash}`,
333
+ )
334
+ .digest("hex")
335
+ .slice(0, 32)}`,
336
+ ownerEmail: currentDocument.ownerEmail,
337
+ documentId: currentDocument.id,
338
+ title: currentDocument.title,
339
+ content: currentDocument.content,
340
+ createdAt: now,
341
+ })
342
+ .onConflictDoNothing();
343
+ await tx
344
+ .update(schema.documents)
345
+ .set({
346
+ content: sourceContent!,
347
+ ...(Object.prototype.hasOwnProperty.call(metadata, "title")
348
+ ? { title: metadata.title ?? "" }
349
+ : {}),
350
+ ...(Object.prototype.hasOwnProperty.call(metadata, "description")
351
+ ? { description: metadata.description ?? "" }
352
+ : {}),
353
+ ...(Object.prototype.hasOwnProperty.call(metadata, "icon")
354
+ ? { icon: metadata.icon }
355
+ : {}),
356
+ sourceUpdatedAt: now,
357
+ updatedAt: now,
358
+ })
359
+ .where(eq(schema.documents.id, target.document.id));
360
+ const [sourceRow] = await tx
361
+ .select()
362
+ .from(schema.contentDatabaseSourceRows)
363
+ .where(
364
+ and(
365
+ eq(schema.contentDatabaseSourceRows.sourceId, target.source.id),
366
+ eq(
367
+ schema.contentDatabaseSourceRows.documentId,
368
+ target.document.id,
369
+ ),
370
+ ),
371
+ );
372
+ if (sourceRow) {
373
+ const values = parseObject(sourceRow.sourceValuesJson);
374
+ await tx
375
+ .update(schema.contentDatabaseSourceRows)
376
+ .set({
377
+ sourceValuesJson: JSON.stringify({
378
+ ...values,
379
+ contentHash: proposedHash,
380
+ metadataHash: hash(
381
+ JSON.stringify({
382
+ title: resolvedTitle,
383
+ description: resolvedDescription,
384
+ icon: resolvedIcon,
385
+ }),
386
+ ),
387
+ }),
388
+ syncState: "linked",
389
+ freshness: "fresh",
390
+ lastSyncedAt: now,
391
+ lastSourceUpdatedAt: now,
392
+ updatedAt: now,
393
+ })
394
+ .where(eq(schema.contentDatabaseSourceRows.id, sourceRow.id));
395
+ }
396
+ }
397
+ const remaining = await tx
398
+ .select({ id: schema.contentDatabaseSourceChangeSets.id })
399
+ .from(schema.contentDatabaseSourceChangeSets)
400
+ .where(
401
+ and(
402
+ eq(
403
+ schema.contentDatabaseSourceChangeSets.sourceId,
404
+ target.source.id,
405
+ ),
406
+ eq(schema.contentDatabaseSourceChangeSets.direction, "incoming"),
407
+ eq(schema.contentDatabaseSourceChangeSets.state, "proposed"),
408
+ ),
409
+ );
410
+ const refreshedSources = await tx
411
+ .update(schema.contentDatabaseSources)
412
+ .set({
413
+ syncState: remaining.length ? "error" : "linked",
414
+ freshness:
415
+ remaining.length || decision === "keep_content" ? "stale" : "fresh",
416
+ lastError: remaining.length
417
+ ? `${remaining.length} file conflict${remaining.length === 1 ? "" : "s"} require review.`
418
+ : decision === "accept_source"
419
+ ? null
420
+ : "Content was kept; push the retained revision to the folder when ready.",
421
+ updatedAt: now,
422
+ })
423
+ .where(eq(schema.contentDatabaseSources.id, target.source.id))
424
+ .returning({ id: schema.contentDatabaseSources.id });
425
+ if (refreshedSources.length !== 1) {
426
+ throw new Error(
427
+ `Local folder source "${target.source.id}" was disconnected during resolution`,
428
+ );
429
+ }
430
+ });
431
+ await writeAppState("refresh-signal", { ts: Date.now() });
432
+ return {
433
+ success: true,
434
+ changeSetId,
435
+ decision,
436
+ documentId: target.document.id,
437
+ sourceId: target.source.id,
438
+ sourceDeleted: sourceDeletion,
439
+ state: decision === "accept_source" ? "applied" : "rejected",
440
+ };
441
+ },
442
+ });