@agent-native/core 0.114.0 → 0.114.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 (162) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/build-compatibility.ts +77 -0
  5. package/corpus/core/src/client/route-chunk-recovery/index.ts +1 -0
  6. package/corpus/core/src/client/use-action.ts +32 -1
  7. package/corpus/core/src/deploy/build.ts +4 -1
  8. package/corpus/core/src/org/context.ts +56 -30
  9. package/corpus/core/src/server/action-routes.ts +52 -2
  10. package/corpus/core/src/server/auth-plugin.ts +9 -4
  11. package/corpus/core/src/server/better-auth-instance.ts +5 -1
  12. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  13. package/corpus/core/src/vite/client.ts +17 -0
  14. package/corpus/templates/content/AGENTS.md +2 -0
  15. package/corpus/templates/content/actions/_content-database-personal-view.ts +49 -5
  16. package/corpus/templates/content/actions/_content-document-access.ts +21 -0
  17. package/corpus/templates/content/actions/_content-favorites.ts +91 -0
  18. package/corpus/templates/content/actions/_content-files.ts +79 -23
  19. package/corpus/templates/content/actions/_content-sidebar-state.ts +24 -0
  20. package/corpus/templates/content/actions/_content-space-access.ts +15 -17
  21. package/corpus/templates/content/actions/_content-space-catalog-guards.ts +20 -0
  22. package/corpus/templates/content/actions/_content-spaces.ts +364 -167
  23. package/corpus/templates/content/actions/_database-source-utils.ts +8 -0
  24. package/corpus/templates/content/actions/_database-utils.ts +249 -15
  25. package/corpus/templates/content/actions/_delete-content-space.ts +84 -0
  26. package/corpus/templates/content/actions/_files-system-properties.ts +499 -0
  27. package/corpus/templates/content/actions/_local-file-documents.ts +0 -1
  28. package/corpus/templates/content/actions/_property-utils.ts +18 -9
  29. package/corpus/templates/content/actions/bind-content-database-source-field.ts +3 -0
  30. package/corpus/templates/content/actions/configure-document-property.ts +3 -0
  31. package/corpus/templates/content/actions/create-content-database.ts +2 -2
  32. package/corpus/templates/content/actions/create-content-space.ts +34 -0
  33. package/corpus/templates/content/actions/create-document.ts +4 -1
  34. package/corpus/templates/content/actions/delete-content-space.ts +19 -0
  35. package/corpus/templates/content/actions/delete-database-items.ts +28 -1
  36. package/corpus/templates/content/actions/delete-document-property.ts +3 -0
  37. package/corpus/templates/content/actions/delete-document.ts +39 -2
  38. package/corpus/templates/content/actions/duplicate-database-item.ts +6 -0
  39. package/corpus/templates/content/actions/duplicate-database-items.ts +6 -0
  40. package/corpus/templates/content/actions/duplicate-document-property.ts +3 -0
  41. package/corpus/templates/content/actions/get-content-database.ts +11 -2
  42. package/corpus/templates/content/actions/get-content-sidebar-state.ts +23 -0
  43. package/corpus/templates/content/actions/get-document.ts +32 -7
  44. package/corpus/templates/content/actions/import-content-source.ts +33 -1
  45. package/corpus/templates/content/actions/list-content-spaces.ts +34 -2
  46. package/corpus/templates/content/actions/list-documents.ts +39 -7
  47. package/corpus/templates/content/actions/reorder-document-property.ts +9 -0
  48. package/corpus/templates/content/actions/set-document-property.ts +41 -23
  49. package/corpus/templates/content/actions/share-local-file-document.ts +15 -0
  50. package/corpus/templates/content/actions/submit-content-database-form.ts +7 -0
  51. package/corpus/templates/content/actions/sync-local-folder-source.ts +14 -0
  52. package/corpus/templates/content/actions/update-content-database-view.ts +1 -0
  53. package/corpus/templates/content/actions/update-content-sidebar-state.ts +27 -0
  54. package/corpus/templates/content/actions/update-document.ts +158 -66
  55. package/corpus/templates/content/app/components/EmptyState.tsx +4 -57
  56. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +8 -1
  57. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +519 -178
  58. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +39 -0
  59. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  60. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +266 -7
  61. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +430 -152
  62. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +467 -66
  63. package/corpus/templates/content/app/components/editor/database/view-config.ts +9 -3
  64. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +720 -196
  65. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +173 -1
  66. package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +1 -1
  67. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +91 -16
  68. package/corpus/templates/content/app/entry.client.tsx +5 -1
  69. package/corpus/templates/content/app/hooks/use-content-database.ts +84 -3
  70. package/corpus/templates/content/app/hooks/use-content-spaces.ts +60 -2
  71. package/corpus/templates/content/app/hooks/use-create-page.ts +30 -0
  72. package/corpus/templates/content/app/hooks/use-documents.ts +270 -19
  73. package/corpus/templates/content/app/i18n-data.ts +243 -20
  74. package/corpus/templates/content/app/lib/local-folder-picker-safety.ts +54 -0
  75. package/corpus/templates/content/app/lib/local-folder-picker-support.ts +18 -0
  76. package/corpus/templates/content/app/routes/_app.favorites.tsx +30 -0
  77. package/corpus/templates/content/app/routes/_app.local-files.tsx +18 -6
  78. package/corpus/templates/content/changelog/2026-07-17-workspace-files-now-stay-in-a-loading-state-until-automatic-.md +6 -0
  79. package/corpus/templates/content/changelog/2026-07-17-workspace-switching-now-opens-the-selected-files-database-pr.md +6 -0
  80. package/corpus/templates/content/changelog/2026-07-17-workspaces-now-expand-above-their-own-files-list-without-dup.md +6 -0
  81. package/corpus/templates/content/changelog/2026-07-18-create-named-workspaces-each-with-its-own-private-files-data.md +6 -0
  82. package/corpus/templates/content/changelog/2026-07-18-favorites-align-with-workspace-and-file-rows-in-the-sidebar.md +6 -0
  83. package/corpus/templates/content/changelog/2026-07-18-local-folder-selection-no-longer-invokes-the-unsafe-native-p.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-18-made-content-workspaces-independently-expandable-restored-or.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-18-page-details-now-live-in-a-shared-info-and-comments-rail-dat.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-18-workspace-database-views-now-control-sidebar-workspace-navig.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-18-workspace-navigation-now-aligns-favorites-with-workspace-pag.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-19-favorites-now-open-as-a-table-and-compact-breadcrumbs-make-w.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-19-files-databases-now-start-unfiltered-sidebar-rows-exactly-fo.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-19-sidebar-page-trees-now-open-only-when-expanded-remember-thei.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-19-workspace-file-databases-now-use-the-workspace-name-open-as-.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-19-workspace-navigation-now-stays-stable-on-hover-uses-folder-i.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-19-workspace-rows-now-use-folder-icons-and-deleting-a-user-crea.md +6 -0
  94. package/corpus/templates/content/changelog/2026-07-19-workspaces-can-be-created-without-leaving-the-workspaces-dat.md +6 -0
  95. package/corpus/templates/content/e2e/global-setup.ts +3 -2
  96. package/corpus/templates/content/parity/matrix.md +28 -28
  97. package/corpus/templates/content/parity/matrix.ts +8 -2
  98. package/corpus/templates/content/server/db/index.ts +1 -0
  99. package/corpus/templates/content/server/db/schema.ts +10 -0
  100. package/corpus/templates/content/server/lib/documents.ts +12 -7
  101. package/corpus/templates/content/server/plugins/db.ts +17 -0
  102. package/corpus/templates/content/shared/api.ts +9 -0
  103. package/corpus/templates/content/shared/database-form.ts +11 -2
  104. package/corpus/templates/content/vite.config.ts +1 -0
  105. package/dist/client/build-compatibility.d.ts +11 -0
  106. package/dist/client/build-compatibility.d.ts.map +1 -0
  107. package/dist/client/build-compatibility.js +56 -0
  108. package/dist/client/build-compatibility.js.map +1 -0
  109. package/dist/client/route-chunk-recovery/index.d.ts +1 -0
  110. package/dist/client/route-chunk-recovery/index.d.ts.map +1 -1
  111. package/dist/client/route-chunk-recovery/index.js +1 -0
  112. package/dist/client/route-chunk-recovery/index.js.map +1 -1
  113. package/dist/client/use-action.d.ts.map +1 -1
  114. package/dist/client/use-action.js +19 -1
  115. package/dist/client/use-action.js.map +1 -1
  116. package/dist/collab/routes.d.ts +1 -1
  117. package/dist/collab/struct-routes.d.ts +1 -1
  118. package/dist/deploy/build.js +2 -1
  119. package/dist/deploy/build.js.map +1 -1
  120. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  121. package/dist/notifications/routes.d.ts +1 -1
  122. package/dist/observability/routes.d.ts +5 -5
  123. package/dist/org/context.d.ts +4 -2
  124. package/dist/org/context.d.ts.map +1 -1
  125. package/dist/org/context.js +44 -22
  126. package/dist/org/context.js.map +1 -1
  127. package/dist/progress/routes.d.ts +1 -1
  128. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  129. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  130. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  131. package/dist/resources/handlers.d.ts +1 -1
  132. package/dist/secrets/routes.d.ts +9 -9
  133. package/dist/server/action-routes.d.ts.map +1 -1
  134. package/dist/server/action-routes.js +32 -2
  135. package/dist/server/action-routes.js.map +1 -1
  136. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  137. package/dist/server/auth-plugin.d.ts.map +1 -1
  138. package/dist/server/auth-plugin.js +9 -5
  139. package/dist/server/auth-plugin.js.map +1 -1
  140. package/dist/server/better-auth-instance.d.ts.map +1 -1
  141. package/dist/server/better-auth-instance.js +4 -1
  142. package/dist/server/better-auth-instance.js.map +1 -1
  143. package/dist/server/transcribe-voice.d.ts +1 -1
  144. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  145. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  146. package/dist/shared/mcp-embed-headers.js +1 -1
  147. package/dist/shared/mcp-embed-headers.js.map +1 -1
  148. package/dist/vite/client.d.ts +6 -0
  149. package/dist/vite/client.d.ts.map +1 -1
  150. package/dist/vite/client.js +8 -0
  151. package/dist/vite/client.js.map +1 -1
  152. package/package.json +1 -1
  153. package/src/client/build-compatibility.ts +77 -0
  154. package/src/client/route-chunk-recovery/index.ts +1 -0
  155. package/src/client/use-action.ts +32 -1
  156. package/src/deploy/build.ts +4 -1
  157. package/src/org/context.ts +56 -30
  158. package/src/server/action-routes.ts +52 -2
  159. package/src/server/auth-plugin.ts +9 -4
  160. package/src/server/better-auth-instance.ts +5 -1
  161. package/src/shared/mcp-embed-headers.ts +1 -1
  162. package/src/vite/client.ts +17 -0
@@ -1,8 +1,10 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { assertAccess } from "@agent-native/core/sharing";
4
+ import { inArray } from "drizzle-orm";
4
5
 
5
- import { getDb } from "../server/db/index.js";
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { assertNotWorkspaceCatalogDocuments } from "./_content-space-catalog-guards.js";
6
8
  import {
7
9
  databaseRowBatchSchema,
8
10
  renumberDatabaseRows,
@@ -20,12 +22,37 @@ export default defineAction({
20
22
  const { database, rows } = await resolveDatabaseRowsForBatch(args);
21
23
 
22
24
  await assertAccess("document", database.documentId, "editor");
25
+ if (database.systemRole === "favorites") {
26
+ const removedItemIds = rows.map((row) => row.item.id);
27
+ const removedDocumentIds = rows.map((row) => row.document.id);
28
+ const now = new Date().toISOString();
29
+ await db.transaction(async (tx) => {
30
+ await tx
31
+ .delete(schema.contentDatabaseItems)
32
+ .where(inArray(schema.contentDatabaseItems.id, removedItemIds));
33
+ await renumberDatabaseRows(
34
+ tx as unknown as ReturnType<typeof getDb>,
35
+ database,
36
+ now,
37
+ );
38
+ });
39
+ await writeAppState("refresh-signal", { ts: Date.now() });
40
+ return {
41
+ ...(await getContentDatabaseResponse(database.id)),
42
+ deletedItemIds: removedItemIds,
43
+ deletedDocumentIds: [],
44
+ deletedCount: 0,
45
+ removedDocumentIds,
46
+ removedCount: removedItemIds.length,
47
+ };
48
+ }
23
49
  for (const row of rows) {
24
50
  await assertAccess("document", row.document.id, "admin");
25
51
  }
26
52
 
27
53
  const deletedItemIds = rows.map((row) => row.item.id);
28
54
  const deletedDocumentIds = rows.map((row) => row.document.id);
55
+ await assertNotWorkspaceCatalogDocuments(db, deletedDocumentIds, "deleted");
29
56
  const now = new Date().toISOString();
30
57
 
31
58
  await db.transaction(async (tx) => {
@@ -44,6 +44,9 @@ export default defineAction({
44
44
  ),
45
45
  );
46
46
  if (!definition) throw new Error(`Property "${propertyId}" not found`);
47
+ if (definition.systemRole) {
48
+ throw new Error("System properties cannot be deleted.");
49
+ }
47
50
 
48
51
  const isBlocks = isBlocksPropertyType(
49
52
  definition.type as DocumentPropertyType,
@@ -6,6 +6,7 @@ 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 { assertNotWorkspaceCatalogDocuments } from "./_content-space-catalog-guards.js";
9
10
 
10
11
  const DELETE_BATCH_SIZE = 90;
11
12
 
@@ -170,6 +171,7 @@ export async function deleteDocumentRecursive(
170
171
  ): Promise<string[]> {
171
172
  const { documentIds, ownedDatabaseIds } =
172
173
  await collectDocumentSubtreeForDelete(db, id, ownerEmail);
174
+ await assertNotWorkspaceCatalogDocuments(db, documentIds, "deleted");
173
175
 
174
176
  const propertyDefinitionIds: string[] = [];
175
177
  await deleteWhereIn(ownedDatabaseIds, async (databaseIdBatch) => {
@@ -355,15 +357,50 @@ export default defineAction({
355
357
  description: "Delete a document and all its children recursively.",
356
358
  schema: z.object({
357
359
  id: z.string().optional().describe("Document ID (required)"),
360
+ databaseDocumentId: z
361
+ .string()
362
+ .optional()
363
+ .describe("Database page the deletion was initiated from"),
358
364
  }),
359
365
  run: async (args) => {
360
366
  const id = args.id;
361
367
  if (!id) throw new Error("--id is required");
362
368
 
369
+ const db = getDb();
370
+ if (args.databaseDocumentId) {
371
+ const [contextDatabase] = await db
372
+ .select()
373
+ .from(schema.contentDatabases)
374
+ .where(
375
+ and(
376
+ eq(schema.contentDatabases.documentId, args.databaseDocumentId),
377
+ eq(schema.contentDatabases.systemRole, "favorites"),
378
+ ),
379
+ );
380
+ if (contextDatabase) {
381
+ await assertAccess("document", contextDatabase.documentId, "editor");
382
+ const [membership] = await db
383
+ .select({ id: schema.contentDatabaseItems.id })
384
+ .from(schema.contentDatabaseItems)
385
+ .where(
386
+ and(
387
+ eq(schema.contentDatabaseItems.databaseId, contextDatabase.id),
388
+ eq(schema.contentDatabaseItems.documentId, id),
389
+ ),
390
+ );
391
+ if (!membership) {
392
+ throw new Error("Document is not part of Favorites");
393
+ }
394
+ await db
395
+ .delete(schema.contentDatabaseItems)
396
+ .where(eq(schema.contentDatabaseItems.id, membership.id));
397
+ await writeAppState("refresh-signal", { ts: Date.now() });
398
+ return { success: true, deleted: 0, removed: 1 };
399
+ }
400
+ }
401
+
363
402
  const access = await assertAccess("document", id, "admin");
364
403
  const existing = access.resource;
365
-
366
- const db = getDb();
367
404
  const [systemDatabase] = await db
368
405
  .select({ systemRole: schema.contentDatabases.systemRole })
369
406
  .from(schema.contentDatabases)
@@ -7,6 +7,7 @@ import { z } from "zod";
7
7
 
8
8
  import { getDb, schema } from "../server/db/index.js";
9
9
  import { ensureDocumentFilesMembership } from "./_content-files.js";
10
+ import { assertNotWorkspaceCatalogDocuments } from "./_content-space-catalog-guards.js";
10
11
  import { getContentDatabaseResponse } from "./_database-utils.js";
11
12
  import { nanoid } from "./_property-utils.js";
12
13
 
@@ -58,6 +59,11 @@ export default defineAction({
58
59
 
59
60
  await assertAccess("document", row.database.documentId, "editor");
60
61
  await assertAccess("document", row.document.id, "viewer");
62
+ await assertNotWorkspaceCatalogDocuments(
63
+ db,
64
+ [row.document.id],
65
+ "duplicated",
66
+ );
61
67
 
62
68
  const now = new Date().toISOString();
63
69
  const nextDocumentId = nanoid();
@@ -6,6 +6,7 @@ import { and, eq, gte, inArray, sql } from "drizzle-orm";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
8
  import { ensureDocumentsFilesMembership } from "./_content-files.js";
9
+ import { assertNotWorkspaceCatalogDocuments } from "./_content-space-catalog-guards.js";
9
10
  import {
10
11
  databaseRowBatchSchema,
11
12
  resolveDatabaseRowsForBatch,
@@ -33,6 +34,11 @@ export default defineAction({
33
34
  }
34
35
 
35
36
  const sourceDocumentIds = rows.map((row) => row.document.id);
37
+ await assertNotWorkspaceCatalogDocuments(
38
+ db,
39
+ sourceDocumentIds,
40
+ "duplicated",
41
+ );
36
42
  const sourceItemIds = rows.map((row) => row.item.id);
37
43
  const now = new Date().toISOString();
38
44
  const insertionPosition =
@@ -48,6 +48,9 @@ export default defineAction({
48
48
  ),
49
49
  );
50
50
  if (!definition) throw new Error(`Property "${propertyId}" not found`);
51
+ if (definition.systemRole) {
52
+ throw new Error("System properties cannot be duplicated.");
53
+ }
51
54
 
52
55
  const now = new Date().toISOString();
53
56
  const newPropertyId = nanoid();
@@ -8,6 +8,7 @@ import type {
8
8
  ContentDatabaseResponse,
9
9
  ContentDatabaseUnavailableResponse,
10
10
  } from "../shared/api.js";
11
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
11
12
  import {
12
13
  CONTENT_DATABASE_MAX_READ_LIMIT,
13
14
  getContentDatabaseResponse,
@@ -65,8 +66,16 @@ export default defineAction({
65
66
  };
66
67
  }
67
68
 
68
- const access = await resolveAccess("document", database.documentId);
69
- if (!access) throw new Error(`Database "${resolvedDatabaseId}" not found`);
69
+ let canRead = Boolean(await resolveAccess("document", database.documentId));
70
+ if (!canRead && database.systemRole === "files" && database.spaceId) {
71
+ try {
72
+ await resolveContentSpaceAccess(database.spaceId);
73
+ canRead = true;
74
+ } catch {
75
+ canRead = false;
76
+ }
77
+ }
78
+ if (!canRead) throw new Error(`Database "${resolvedDatabaseId}" not found`);
70
79
 
71
80
  if (database.deletedAt) {
72
81
  return {
@@ -0,0 +1,23 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { getUserSetting } from "@agent-native/core/settings";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ CONTENT_SIDEBAR_STATE_SETTING_KEY,
7
+ normalizeContentSidebarState,
8
+ } from "./_content-sidebar-state.js";
9
+
10
+ export default defineAction({
11
+ description: "Read the current user's Content sidebar expansion state.",
12
+ schema: z.object({}),
13
+ http: { method: "GET" },
14
+ agentTool: false,
15
+ run: async (_args, ctx) => {
16
+ if (!ctx?.userEmail) throw new Error("Not authenticated.");
17
+ const stored = await getUserSetting(
18
+ ctx.userEmail,
19
+ CONTENT_SIDEBAR_STATE_SETTING_KEY,
20
+ );
21
+ return { state: normalizeContentSidebarState(stored) };
22
+ },
23
+ });
@@ -1,12 +1,14 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { buildDeepLink } from "@agent-native/core/server";
3
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
4
  import { resolveAccess } from "@agent-native/core/sharing";
5
+ import { eq } from "drizzle-orm";
4
6
  import { z } from "zod";
5
7
 
6
- import {
7
- parseDocumentFavorite,
8
- parseDocumentHideFromSearch,
9
- } from "../server/lib/documents.js";
8
+ import { getDb, schema } from "../server/db/index.js";
9
+ import { parseDocumentHideFromSearch } from "../server/lib/documents.js";
10
+ import { favoriteDocumentIds } from "./_content-favorites.js";
11
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
10
12
  import {
11
13
  getDatabaseByDocumentId,
12
14
  getDocumentContextPath,
@@ -15,7 +17,6 @@ import {
15
17
  serializeDatabaseMembership,
16
18
  } from "./_database-utils.js";
17
19
  import { serializeDocumentSource } from "./_document-source.js";
18
- import "../server/db/index.js";
19
20
  import {
20
21
  listPropertiesForDocument,
21
22
  serializeDatabase,
@@ -29,6 +30,26 @@ function canManageRole(role: string) {
29
30
  return role === "owner" || role === "admin";
30
31
  }
31
32
 
33
+ async function resolveDocumentAccess(id: string) {
34
+ const current = await resolveAccess("document", id);
35
+ if (current) return current;
36
+ const [reference] = await getDb()
37
+ .select({ spaceId: schema.documents.spaceId })
38
+ .from(schema.documents)
39
+ .where(eq(schema.documents.id, id))
40
+ .limit(1);
41
+ if (!reference?.spaceId) return null;
42
+ try {
43
+ const spaceAccess = await resolveContentSpaceAccess(reference.spaceId);
44
+ return resolveAccess("document", id, {
45
+ userEmail: spaceAccess.authority.userEmail,
46
+ orgId: spaceAccess.authority.orgId ?? undefined,
47
+ });
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
32
53
  export default defineAction({
33
54
  description: "Get a single document by ID with full content.",
34
55
  schema: z.object({
@@ -40,7 +61,7 @@ export default defineAction({
40
61
  run: async (args) => {
41
62
  if (!args.id) throw new Error("--id is required");
42
63
 
43
- const access = await resolveAccess("document", args.id);
64
+ const access = await resolveDocumentAccess(args.id);
44
65
  // Not-found is a deterministic client-state condition (deleted or
45
66
  // inaccessible document still referenced by an open tab) — 404, not a
46
67
  // 500 that floods the console as Internal Server Error.
@@ -57,6 +78,10 @@ export default defineAction({
57
78
  const doc = access.resource;
58
79
  const database = await getDatabaseByDocumentId(doc.id);
59
80
  const databaseMembership = await getDatabaseItemByDocumentId(doc.id);
81
+ const userEmail = getRequestUserEmail();
82
+ const favoriteIds = userEmail
83
+ ? await favoriteDocumentIds(getDb(), userEmail, [doc.id])
84
+ : new Set<string>();
60
85
 
61
86
  return {
62
87
  id: doc.id,
@@ -71,7 +96,7 @@ export default defineAction({
71
96
  description: doc.description,
72
97
  icon: doc.icon,
73
98
  position: doc.position,
74
- isFavorite: parseDocumentFavorite(doc.isFavorite),
99
+ isFavorite: favoriteIds.has(doc.id),
75
100
  hideFromSearch: parseDocumentHideFromSearch(doc.hideFromSearch),
76
101
  visibility: doc.visibility,
77
102
  source: serializeDocumentSource(doc),
@@ -15,6 +15,10 @@ import {
15
15
  parseContentSourceFile,
16
16
  type ParsedContentSourceFile,
17
17
  } from "../shared/content-source.js";
18
+ import {
19
+ favoriteDocumentIds,
20
+ setFavoriteMembership,
21
+ } from "./_content-favorites.js";
18
22
  import { ensureDocumentsFilesMembership } from "./_content-files.js";
19
23
  import { resolveContentSpaceAccess } from "./_content-space-access.js";
20
24
  import {
@@ -231,6 +235,16 @@ export default defineAction({
231
235
  .filter((document) => document.sourcePath)
232
236
  .map((document) => [document.sourcePath!, document]),
233
237
  );
238
+ const existingFavoriteIds = await favoriteDocumentIds(
239
+ db,
240
+ currentUserEmail,
241
+ [
242
+ ...new Set([
243
+ ...parsed.flatMap((file) => (file.id ? [file.id] : [])),
244
+ ...existingLocalDocs.map((document) => document.id),
245
+ ]),
246
+ ],
247
+ );
234
248
 
235
249
  const seenIds = new Set<string>();
236
250
  const duplicateIds = new Set<string>();
@@ -311,7 +325,7 @@ export default defineAction({
311
325
  file.icon !== undefined && file.icon !== existing.icon;
312
326
  const favoriteChanged =
313
327
  file.isFavorite !== undefined &&
314
- boolToInt(file.isFavorite) !== (existing.isFavorite ?? 0);
328
+ file.isFavorite !== existingFavoriteIds.has(id);
315
329
  const discoverabilityChanged =
316
330
  file.hideFromSearch !== undefined &&
317
331
  boolToInt(file.hideFromSearch) !== (existing.hideFromSearch ?? 0);
@@ -363,6 +377,15 @@ export default defineAction({
363
377
  .update(schema.documents)
364
378
  .set(updates)
365
379
  .where(eq(schema.documents.id, id));
380
+ if (favoriteChanged) {
381
+ await setFavoriteMembership({
382
+ db,
383
+ userEmail: currentUserEmail,
384
+ documentId: id,
385
+ favorite: file.isFavorite === true,
386
+ now,
387
+ });
388
+ }
366
389
  }
367
390
 
368
391
  updated.push({ id, path: file.path, title: file.title });
@@ -389,6 +412,15 @@ export default defineAction({
389
412
  createdAt: now,
390
413
  updatedAt: now,
391
414
  });
415
+ if (file.isFavorite) {
416
+ await setFavoriteMembership({
417
+ db,
418
+ userEmail: currentUserEmail,
419
+ documentId: id,
420
+ favorite: true,
421
+ now,
422
+ });
423
+ }
392
424
  } catch (err) {
393
425
  errors.push({
394
426
  path: file.path,
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
- import { and, eq, isNull } from "drizzle-orm";
3
+ import { and, eq, inArray, isNull } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
@@ -26,6 +26,7 @@ export default defineAction({
26
26
  const db = getDb();
27
27
  const personalSpaceId = personalContentSpaceId(email);
28
28
  const catalogIds = systemIdsForContentSpace(personalSpaceId, "workspaces");
29
+ const favoritesIds = systemIdsForContentSpace(personalSpaceId, "favorites");
29
30
  const memberships = await listContentOrganizationMemberships(email);
30
31
  const roleByOrgId = new Map(
31
32
  memberships.map((membership) => [
@@ -65,11 +66,26 @@ export default defineAction({
65
66
  isNull(schema.contentSpaces.archivedAt),
66
67
  ),
67
68
  );
69
+ const filesDatabaseIds = rows.map((row) => row.space.filesDatabaseId);
70
+ const databaseIds = [...filesDatabaseIds, favoritesIds.databaseId];
71
+ const filesDatabases = databaseIds.length
72
+ ? await db
73
+ .select({
74
+ id: schema.contentDatabases.id,
75
+ documentId: schema.contentDatabases.documentId,
76
+ })
77
+ .from(schema.contentDatabases)
78
+ .where(inArray(schema.contentDatabases.id, databaseIds))
79
+ : [];
80
+ const filesDocumentIdByDatabaseId = new Map(
81
+ filesDatabases.map((database) => [database.id, database.documentId]),
82
+ );
68
83
  const spaces = [] as Array<{
69
84
  id: string;
70
85
  name: string;
71
86
  kind: string;
72
87
  filesDatabaseId: string;
88
+ filesDocumentId: string;
73
89
  orgId: string | null;
74
90
  role: string;
75
91
  catalogItemId: string;
@@ -87,17 +103,33 @@ export default defineAction({
87
103
  ? "owner"
88
104
  : undefined;
89
105
  if (!role) continue;
106
+ const filesDocumentId = filesDocumentIdByDatabaseId.get(
107
+ row.space.filesDatabaseId,
108
+ );
109
+ if (!filesDocumentId) continue;
90
110
  spaces.push({
91
111
  id: row.space.id,
92
112
  name: row.space.name,
93
113
  kind: row.space.kind,
94
114
  filesDatabaseId: row.space.filesDatabaseId,
115
+ filesDocumentId,
95
116
  orgId: row.space.orgId,
96
117
  role,
97
118
  catalogItemId: row.mapping.databaseItemId,
98
119
  catalogDocumentId: row.mapping.documentId,
99
120
  });
100
121
  }
101
- return { catalogDatabaseId: catalogIds.databaseId, spaces };
122
+ return {
123
+ catalogDatabaseId: catalogIds.databaseId,
124
+ catalogDocumentId: catalogIds.documentId,
125
+ favoritesDatabaseId: filesDocumentIdByDatabaseId.has(
126
+ favoritesIds.databaseId,
127
+ )
128
+ ? favoritesIds.databaseId
129
+ : null,
130
+ favoritesDocumentId:
131
+ filesDocumentIdByDatabaseId.get(favoritesIds.databaseId) ?? null,
132
+ spaces,
133
+ };
102
134
  },
103
135
  });
@@ -14,9 +14,10 @@ import { z } from "zod";
14
14
  import { getDb, schema } from "../server/db/index.js";
15
15
  import {
16
16
  documentDiscoveryFilter,
17
- parseDocumentFavorite,
18
17
  parseDocumentHideFromSearch,
19
18
  } from "../server/lib/documents.js";
19
+ import { favoriteDocumentIds } from "./_content-favorites.js";
20
+ import { listContentOrganizationMemberships } from "./_content-space-access.js";
20
21
  import { serializeDatabaseMembership } from "./_database-utils.js";
21
22
  import { serializeDocumentSource } from "./_document-source.js";
22
23
  import { parseDatabaseViewConfig } from "./_property-utils.js";
@@ -50,7 +51,23 @@ export default defineAction({
50
51
  run: async () => {
51
52
  const db = getDb();
52
53
  const userEmail = getRequestUserEmail();
53
- const orgId = getRequestOrgId();
54
+ const activeOrgId = getRequestOrgId();
55
+ const memberships = userEmail
56
+ ? await listContentOrganizationMemberships(userEmail)
57
+ : [];
58
+ const authorizedOrgIds = [
59
+ ...new Set([
60
+ ...memberships.map((membership) => membership.orgId),
61
+ ...(!userEmail && activeOrgId ? [activeOrgId] : []),
62
+ ]),
63
+ ];
64
+ const accessContexts = [
65
+ { userEmail: userEmail ?? undefined },
66
+ ...authorizedOrgIds.map((orgId) => ({
67
+ userEmail: userEmail ?? undefined,
68
+ orgId,
69
+ })),
70
+ ];
54
71
  // Projection that deliberately avoids pulling the full `content` blob:
55
72
  // document bodies can be multi-MB, and the list/tree path only needs a
56
73
  // short preview plus the true length. `substr` truncates the transferred
@@ -84,8 +101,15 @@ export default defineAction({
84
101
  .from(schema.documents)
85
102
  .where(
86
103
  and(
87
- accessFilter(schema.documents, schema.documentShares),
88
- documentDiscoveryFilter(),
104
+ or(
105
+ ...accessContexts.map((context) =>
106
+ accessFilter(schema.documents, schema.documentShares, context),
107
+ ),
108
+ ),
109
+ documentDiscoveryFilter({
110
+ userEmail,
111
+ orgIds: authorizedOrgIds,
112
+ }),
89
113
  ),
90
114
  )
91
115
  .orderBy(asc(schema.documents.position));
@@ -104,6 +128,13 @@ export default defineAction({
104
128
  }
105
129
  >();
106
130
  const softDeletedDocumentIds = new Set<string>();
131
+ const favoriteIds = userEmail
132
+ ? await favoriteDocumentIds(
133
+ db,
134
+ userEmail,
135
+ documents.map((document) => document.id),
136
+ )
137
+ : new Set<string>();
107
138
 
108
139
  if (documents.length > 0) {
109
140
  const visibleDocumentIds = documents.map((d) => d.id);
@@ -117,7 +148,7 @@ export default defineAction({
117
148
  )!,
118
149
  );
119
150
  }
120
- if (orgId) {
151
+ for (const orgId of authorizedOrgIds) {
121
152
  principalClauses.push(
122
153
  and(
123
154
  eq(schema.documentShares.principalType, "org"),
@@ -170,6 +201,7 @@ export default defineAction({
170
201
  )
171
202
  .orderBy(
172
203
  sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
204
+ sql`CASE WHEN ${schema.contentDatabases.systemRole} = 'files' THEN 0 ELSE 1 END`,
173
205
  asc(schema.contentDatabases.id),
174
206
  ),
175
207
  db
@@ -277,7 +309,7 @@ export default defineAction({
277
309
  if (
278
310
  userEmail &&
279
311
  d.ownerEmail === userEmail &&
280
- (orgId ? d.orgId === orgId : !d.orgId)
312
+ (!d.orgId || authorizedOrgIds.includes(d.orgId))
281
313
  ) {
282
314
  accessRole = "owner";
283
315
  }
@@ -291,7 +323,7 @@ export default defineAction({
291
323
  contentLength: Number(d.contentLength) || 0,
292
324
  icon: d.icon,
293
325
  position: d.position,
294
- isFavorite: parseDocumentFavorite(d.isFavorite),
326
+ isFavorite: favoriteIds.has(d.id),
295
327
  hideFromSearch: parseDocumentHideFromSearch(d.hideFromSearch),
296
328
  notionPageId: notionPageIdByDocumentId.get(d.id) ?? null,
297
329
  notionPageUrl: notionPageIdByDocumentId.has(d.id)
@@ -42,6 +42,15 @@ export default defineAction({
42
42
  if (!ids.includes(targetPropertyId)) {
43
43
  throw new Error(`Property "${targetPropertyId}" not found`);
44
44
  }
45
+ const definitionById = new Map(
46
+ definitions.map((definition) => [definition.id, definition]),
47
+ );
48
+ if (
49
+ definitionById.get(propertyId)?.systemRole ||
50
+ definitionById.get(targetPropertyId)?.systemRole
51
+ ) {
52
+ throw new Error("System properties cannot be reordered.");
53
+ }
45
54
  if (propertyId === targetPropertyId) {
46
55
  return {
47
56
  documentId,