@agent-native/core 0.157.1 → 0.157.3

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 (24) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/server/handlers/bookings.ts +6 -1
  3. package/corpus/templates/clips/actions/request-recording-access.ts +47 -14
  4. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +125 -15
  5. package/corpus/templates/content/actions/add-database-item.ts +9 -0
  6. package/corpus/templates/content/actions/describe-content-database.ts +92 -0
  7. package/corpus/templates/content/actions/list-content-databases.ts +213 -72
  8. package/corpus/templates/content/actions/set-document-property.ts +9 -0
  9. package/corpus/templates/content/actions/submit-content-database-form.ts +9 -0
  10. package/corpus/templates/content/actions/update-document.ts +9 -0
  11. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +7 -0
  12. package/corpus/templates/content/app/components/editor/database/settings.tsx +7 -0
  13. package/corpus/templates/content/app/hooks/use-content-database.ts +91 -11
  14. package/corpus/templates/content/parity/matrix.md +1 -1
  15. package/corpus/templates/content/parity/matrix.ts +7 -1
  16. package/corpus/templates/content/server/plugins/agent-chat.ts +3 -0
  17. package/corpus/templates/content/shared/api.ts +9 -0
  18. package/dist/collab/awareness.d.ts +2 -2
  19. package/dist/notifications/routes.d.ts +3 -3
  20. package/dist/observability/routes.d.ts +5 -5
  21. package/dist/secrets/routes.d.ts +9 -9
  22. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  23. package/docs/content/cloneable-saas.mdx +24 -26
  24. package/package.json +1 -1
@@ -1,20 +1,55 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, asc, eq, isNull, ne, sql } from "drizzle-orm";
3
+ import {
4
+ and,
5
+ asc,
6
+ eq,
7
+ inArray,
8
+ isNull,
9
+ notInArray,
10
+ or,
11
+ sql,
12
+ } from "drizzle-orm";
4
13
  import { z } from "zod";
5
14
 
6
15
  import { getDb, schema } from "../server/db/index.js";
7
16
  import { documentDiscoveryFilter } from "../server/lib/documents.js";
8
17
  import type { ListContentDatabasesResponse } from "../shared/api.js";
18
+ import { documentDiscoveryPagination } from "./_document-discovery-query.js";
19
+
20
+ const DEFAULT_CONTENT_DATABASE_DISCOVERY_LIMIT = 50;
9
21
 
10
22
  function escapeLike(s: string): string {
11
23
  return s.replace(/([\\%_])/g, "\\$1");
12
24
  }
13
25
 
26
+ export class ContentDatabaseResolutionError extends Error {}
27
+
14
28
  export default defineAction({
15
29
  description:
16
- "List the content databases the user can access (owned, shared, or org-shared — matching the sidebar) so any of them can be used as a local-table source. Optionally filters by title or excludes one database (e.g. the one being configured).",
30
+ "Discover one bounded page of ordinary Content databases the user can access from their live title and user-authored description. Returns stable database, document, and space IDs with explicit pagination; follow nextOffset until hasMore is false. Use exact filters before reading a selected database's schema.",
17
31
  schema: z.object({
32
+ spaceId: z
33
+ .string()
34
+ .min(1)
35
+ .optional()
36
+ .describe("Exact Content space ID to search within."),
37
+ databaseId: z
38
+ .string()
39
+ .min(1)
40
+ .optional()
41
+ .describe("Exact Content database ID to resolve."),
42
+ documentId: z
43
+ .string()
44
+ .min(1)
45
+ .optional()
46
+ .describe("Exact Content database document/page ID to resolve."),
47
+ title: z
48
+ .string()
49
+ .trim()
50
+ .min(1)
51
+ .optional()
52
+ .describe("Exact live database title to resolve, case-insensitively."),
18
53
  excludeDatabaseId: z
19
54
  .string()
20
55
  .optional()
@@ -23,63 +58,43 @@ export default defineAction({
23
58
  .array(z.string())
24
59
  .optional()
25
60
  .describe("Database ids to omit from the results."),
26
- query: z.string().optional().describe("Optional title search text."),
61
+ query: z
62
+ .string()
63
+ .optional()
64
+ .describe("Optional title or user-authored description search text."),
27
65
  limit: z.coerce
28
66
  .number()
29
67
  .int()
30
68
  .min(1)
31
69
  .max(50)
32
- .optional()
33
- .describe("Maximum number of databases to return."),
70
+ .default(DEFAULT_CONTENT_DATABASE_DISCOVERY_LIMIT)
71
+ .describe("Maximum number of databases to return. Defaults to 50."),
72
+ offset: z.coerce
73
+ .number()
74
+ .int()
75
+ .min(0)
76
+ .default(0)
77
+ .describe("Zero-based continuation offset."),
34
78
  }),
35
79
  http: { method: "GET" },
36
80
  readOnly: true,
81
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
37
82
  run: async (args): Promise<ListContentDatabasesResponse> => {
38
83
  const db = getDb();
39
84
  const query = args.query?.trim();
40
85
  const pattern = query ? `%${escapeLike(query.toLowerCase())}%` : null;
86
+ const exactTitle = args.title?.toLowerCase();
87
+ const resolvesExactly = !!(
88
+ args.databaseId ||
89
+ args.documentId ||
90
+ exactTitle
91
+ );
41
92
  const excludedDatabaseIds = new Set(
42
93
  [
43
94
  args.excludeDatabaseId?.trim(),
44
95
  ...(args.excludeDatabaseIds ?? []).map((id) => id.trim()),
45
96
  ].filter((id): id is string => !!id),
46
97
  );
47
- // The same access + discovery filter the sidebar uses, so the picker shows
48
- // owned AND shared/org databases and never a trashed/hidden one.
49
- const queryBuilder = db
50
- .select({
51
- id: schema.contentDatabases.id,
52
- documentId: schema.contentDatabases.documentId,
53
- title: schema.documents.title,
54
- })
55
- .from(schema.contentDatabases)
56
- .innerJoin(
57
- schema.documents,
58
- eq(schema.contentDatabases.documentId, schema.documents.id),
59
- )
60
- .where(
61
- and(
62
- accessFilter(schema.documents, schema.documentShares),
63
- isNull(schema.documents.trashedAt),
64
- documentDiscoveryFilter(),
65
- isNull(schema.contentDatabases.deletedAt),
66
- excludedDatabaseIds.size === 1
67
- ? ne(
68
- schema.contentDatabases.id,
69
- Array.from(excludedDatabaseIds)[0]!,
70
- )
71
- : undefined,
72
- pattern
73
- ? sql`lower(${schema.documents.title}) LIKE ${pattern} ESCAPE '\\'`
74
- : undefined,
75
- ),
76
- )
77
- .orderBy(asc(schema.documents.position));
78
-
79
- const rows = args.limit
80
- ? await queryBuilder.limit(args.limit)
81
- : await queryBuilder;
82
-
83
98
  const localTableSources =
84
99
  excludedDatabaseIds.size > 0
85
100
  ? await db
@@ -90,39 +105,165 @@ export default defineAction({
90
105
  .from(schema.contentDatabaseSources)
91
106
  .where(eq(schema.contentDatabaseSources.sourceType, "local-table"))
92
107
  : [];
93
- const localTableTargetByDatabaseId = new Map(
94
- localTableSources.map((source) => [
95
- source.databaseId,
96
- source.sourceTable,
97
- ]),
98
- );
99
- const sourceChainIncludesExcludedDatabase = (databaseId: string) => {
100
- const seen = new Set<string>();
101
- let current: string | undefined = databaseId;
102
- while (current && !seen.has(current)) {
103
- if (excludedDatabaseIds.has(current)) return true;
104
- seen.add(current);
105
- current = localTableTargetByDatabaseId.get(current);
108
+ const excludedDatabaseRows =
109
+ excludedDatabaseIds.size > 0
110
+ ? await db
111
+ .select({ id: schema.contentDatabases.id })
112
+ .from(schema.contentDatabases)
113
+ .innerJoin(
114
+ schema.documents,
115
+ eq(schema.contentDatabases.documentId, schema.documents.id),
116
+ )
117
+ .where(
118
+ and(
119
+ accessFilter(schema.documents, schema.documentShares),
120
+ or(
121
+ inArray(
122
+ schema.contentDatabases.id,
123
+ Array.from(excludedDatabaseIds),
124
+ ),
125
+ inArray(
126
+ schema.contentDatabases.documentId,
127
+ Array.from(excludedDatabaseIds),
128
+ ),
129
+ ),
130
+ ),
131
+ )
132
+ : [];
133
+ const excludedSourceChainDatabaseIds = new Set([
134
+ ...excludedDatabaseIds,
135
+ ...excludedDatabaseRows.map((row) => row.id),
136
+ ]);
137
+ let expandedSourceChain = true;
138
+ while (expandedSourceChain) {
139
+ expandedSourceChain = false;
140
+ for (const source of localTableSources) {
141
+ if (
142
+ excludedSourceChainDatabaseIds.has(source.sourceTable) &&
143
+ !excludedSourceChainDatabaseIds.has(source.databaseId)
144
+ ) {
145
+ excludedSourceChainDatabaseIds.add(source.databaseId);
146
+ expandedSourceChain = true;
147
+ }
106
148
  }
107
- return false;
108
- };
149
+ }
109
150
 
110
- const databases = rows
111
- // Exclusion ids may be database ids OR database document ids — the
112
- // settings panel only has the document id before any source exists.
113
- .filter(
114
- (row) =>
115
- !excludedDatabaseIds.has(row.documentId) &&
116
- !sourceChainIncludesExcludedDatabase(row.id),
117
- )
118
- .map((row) => ({
119
- databaseId: row.id,
120
- documentId: row.documentId,
121
- // The document's live title (matches the sidebar) rather than the
122
- // possibly-stale content_databases.title.
123
- title: row.title ?? "Untitled database",
124
- }));
151
+ // The same access + discovery filter the sidebar uses, so the picker shows
152
+ // owned AND shared/org databases and never a trashed/hidden one. Resolve
153
+ // source-chain exclusions before limiting so every page is truthfully full.
154
+ const where = and(
155
+ accessFilter(schema.documents, schema.documentShares),
156
+ isNull(schema.documents.trashedAt),
157
+ documentDiscoveryFilter(),
158
+ or(
159
+ eq(schema.documents.hideFromSearch, 0),
160
+ isNull(schema.documents.hideFromSearch),
161
+ ),
162
+ isNull(schema.contentDatabases.deletedAt),
163
+ isNull(schema.contentDatabases.systemRole),
164
+ args.spaceId
165
+ ? eq(schema.contentDatabases.spaceId, args.spaceId)
166
+ : undefined,
167
+ args.databaseId
168
+ ? eq(schema.contentDatabases.id, args.databaseId)
169
+ : undefined,
170
+ args.documentId
171
+ ? eq(schema.contentDatabases.documentId, args.documentId)
172
+ : undefined,
173
+ exactTitle
174
+ ? sql`lower(${schema.documents.title}) = ${exactTitle}`
175
+ : undefined,
176
+ excludedSourceChainDatabaseIds.size > 0
177
+ ? notInArray(
178
+ schema.contentDatabases.id,
179
+ Array.from(excludedSourceChainDatabaseIds),
180
+ )
181
+ : undefined,
182
+ excludedDatabaseIds.size > 0
183
+ ? notInArray(
184
+ schema.contentDatabases.documentId,
185
+ Array.from(excludedDatabaseIds),
186
+ )
187
+ : undefined,
188
+ pattern
189
+ ? or(
190
+ sql`lower(${schema.documents.title}) LIKE ${pattern} ESCAPE '\\'`,
191
+ sql`lower(${schema.documents.description}) LIKE ${pattern} ESCAPE '\\'`,
192
+ )
193
+ : undefined,
194
+ );
195
+ const baseQuery = () =>
196
+ db
197
+ .select({
198
+ id: schema.contentDatabases.id,
199
+ documentId: schema.contentDatabases.documentId,
200
+ title: schema.documents.title,
201
+ description: schema.documents.description,
202
+ spaceId: schema.contentDatabases.spaceId,
203
+ })
204
+ .from(schema.contentDatabases)
205
+ .innerJoin(
206
+ schema.documents,
207
+ eq(schema.contentDatabases.documentId, schema.documents.id),
208
+ )
209
+ .where(where)
210
+ .orderBy(
211
+ asc(schema.documents.position),
212
+ asc(schema.contentDatabases.id),
213
+ );
214
+
215
+ // Two visible matches are sufficient to reject an exact selector without
216
+ // materializing every duplicate-title row.
217
+ const rows = resolvesExactly
218
+ ? await baseQuery().limit(2)
219
+ : await baseQuery().limit(args.limit).offset(args.offset);
220
+
221
+ if (resolvesExactly && rows.length !== 1) {
222
+ const selector = args.databaseId
223
+ ? `database ID "${args.databaseId}"`
224
+ : args.documentId
225
+ ? `document ID "${args.documentId}"`
226
+ : `title "${args.title?.trim()}"`;
227
+ throw new ContentDatabaseResolutionError(
228
+ rows.length === 0
229
+ ? `No accessible Content database matched exact ${selector}.`
230
+ : `Exact ${selector} is ambiguous across multiple accessible Content databases.`,
231
+ );
232
+ }
125
233
 
126
- return { databases };
234
+ const databases = rows.map((row) => ({
235
+ databaseId: row.id,
236
+ documentId: row.documentId,
237
+ spaceId: row.spaceId,
238
+ // The document's live title (matches the sidebar) rather than the
239
+ // possibly-stale content_databases.title.
240
+ title: row.title ?? "Untitled database",
241
+ description: row.description,
242
+ }));
243
+
244
+ const totalItems = resolvesExactly
245
+ ? databases.length
246
+ : Number(
247
+ (
248
+ await db
249
+ .select({ count: sql<number>`count(*)` })
250
+ .from(schema.contentDatabases)
251
+ .innerJoin(
252
+ schema.documents,
253
+ eq(schema.contentDatabases.documentId, schema.documents.id),
254
+ )
255
+ .where(where)
256
+ )[0]?.count ?? 0,
257
+ );
258
+
259
+ return {
260
+ databases,
261
+ pagination: documentDiscoveryPagination({
262
+ offset: resolvesExactly ? 0 : args.offset,
263
+ limit: args.limit,
264
+ totalItems,
265
+ returnedItems: databases.length,
266
+ }),
267
+ };
127
268
  },
128
269
  });
@@ -28,6 +28,15 @@ import {
28
28
 
29
29
  export default defineAction({
30
30
  description: "Set a Notion-style property value on a document.",
31
+ publicAgent: {
32
+ expose: true,
33
+ readOnly: false,
34
+ requiresAuth: true,
35
+ isConsequential: true,
36
+ title: "Set Content Document Property",
37
+ description:
38
+ "Delegate one property update on an existing Content database document.",
39
+ },
31
40
  schema: z.object({
32
41
  documentId: z.string().describe("Document ID (required)"),
33
42
  databaseId: z
@@ -195,6 +195,15 @@ function resolveSubmittedProperties(
195
195
  export default defineAction({
196
196
  description:
197
197
  "Submit one row through a Content database form. Validates that form's required questions, resolves option labels safely, writes the title, Blocks, and property values atomically, verifies the saved row, and returns its exact page link.",
198
+ publicAgent: {
199
+ expose: true,
200
+ readOnly: false,
201
+ requiresAuth: true,
202
+ isConsequential: true,
203
+ title: "Submit Content Database Form",
204
+ description:
205
+ "Delegate a validated, atomic submission to an existing Content database form.",
206
+ },
198
207
  schema: submitContentDatabaseFormSchema,
199
208
  mcpApp: {
200
209
  compactCatalog: true,
@@ -290,6 +290,15 @@ export function isStaleBuilderImageSourceComponentSave(args: {
290
290
  export default defineAction({
291
291
  description:
292
292
  "Update an existing document's title, content, icon, or favorite status.",
293
+ publicAgent: {
294
+ expose: true,
295
+ readOnly: false,
296
+ requiresAuth: true,
297
+ isConsequential: true,
298
+ title: "Update Content Document",
299
+ description:
300
+ "Delegate a sparse update to an existing Content document while preserving omitted fields.",
301
+ },
293
302
  schema: z.object({
294
303
  id: z.string().optional().describe("Document ID (required)"),
295
304
  title: z.string().optional().describe("New title"),
@@ -113,6 +113,7 @@ import {
113
113
  import { Link, useLocation, useNavigate } from "react-router";
114
114
  import { toast } from "sonner";
115
115
 
116
+ import { QueryErrorState } from "@/components/QueryErrorState";
116
117
  import {
117
118
  contentSpaceForCatalogItem,
118
119
  createContentSpaceSelectionQueue,
@@ -9265,6 +9266,12 @@ function AddSourceView({
9265
9266
  <Spinner className="size-3.5" />
9266
9267
  {dbText("loadingTables")}
9267
9268
  </div>
9269
+ ) : query.isError ? (
9270
+ <QueryErrorState
9271
+ compact
9272
+ onRetry={() => void query.refetch()}
9273
+ retrying={query.isFetching}
9274
+ />
9268
9275
  ) : tables.length === 0 ? (
9269
9276
  <div className="min-w-0 break-words px-2 text-xs text-muted-foreground">
9270
9277
  {dbText("noOtherDatabasesAvailableToAdd")}
@@ -52,6 +52,7 @@ import {
52
52
  } from "react";
53
53
  import { toast } from "sonner";
54
54
 
55
+ import { QueryErrorState } from "@/components/QueryErrorState";
55
56
  import { Button } from "@/components/ui/button";
56
57
  import {
57
58
  DropdownMenu,
@@ -1543,6 +1544,12 @@ function AddSourceView({
1543
1544
  <Spinner className="size-3.5" />
1544
1545
  {dbText("loadingTables")}
1545
1546
  </div>
1547
+ ) : query.isError ? (
1548
+ <QueryErrorState
1549
+ compact
1550
+ onRetry={() => void query.refetch()}
1551
+ retrying={query.isFetching}
1552
+ />
1546
1553
  ) : tables.length === 0 ? (
1547
1554
  <div className="min-w-0 break-words px-2 text-xs text-muted-foreground">
1548
1555
  {dbText("noOtherDatabasesAvailableToAdd")}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ callAction,
2
3
  useActionMutation,
3
4
  useActionQuery,
4
5
  } from "@agent-native/core/client/hooks";
@@ -56,7 +57,7 @@ import type {
56
57
  ValidateBuilderSourceExecutionRequest,
57
58
  } from "@shared/api";
58
59
  import type { Query, QueryClient } from "@tanstack/react-query";
59
- import { useQueryClient } from "@tanstack/react-query";
60
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
60
61
 
61
62
  import { documentQueryFilter } from "../lib/document-query";
62
63
 
@@ -1390,16 +1391,95 @@ export function useContentDatabases(args: {
1390
1391
  excludeDatabaseIds?: string[];
1391
1392
  enabled: boolean;
1392
1393
  }) {
1393
- return useActionQuery<ListContentDatabasesResponse>(
1394
- "list-content-databases",
1395
- args.enabled
1396
- ? {
1397
- excludeDatabaseId: args.excludeDatabaseId ?? undefined,
1398
- excludeDatabaseIds: args.excludeDatabaseIds ?? undefined,
1399
- }
1400
- : undefined,
1401
- { enabled: args.enabled, retry: false },
1402
- );
1394
+ const filters = {
1395
+ excludeDatabaseId: args.excludeDatabaseId ?? undefined,
1396
+ excludeDatabaseIds: args.excludeDatabaseIds ?? undefined,
1397
+ };
1398
+ return useQuery({
1399
+ queryKey: ["action", "list-content-databases", filters],
1400
+ queryFn: async ({ signal }) => ({
1401
+ databases: await fetchCompleteContentDatabaseList((offset, limit) =>
1402
+ callAction<ListContentDatabasesResponse>(
1403
+ "list-content-databases",
1404
+ { ...filters, offset, limit },
1405
+ { method: "GET", signal },
1406
+ ),
1407
+ ),
1408
+ }),
1409
+ enabled: args.enabled,
1410
+ retry: false,
1411
+ });
1412
+ }
1413
+
1414
+ const CONTENT_DATABASE_LIST_PAGE_SIZE = 50;
1415
+
1416
+ export async function fetchCompleteContentDatabaseList(
1417
+ fetchPage: (
1418
+ offset: number,
1419
+ limit: number,
1420
+ ) => Promise<ListContentDatabasesResponse>,
1421
+ ) {
1422
+ const databases: ListContentDatabasesResponse["databases"] = [];
1423
+ const databaseIds = new Set<string>();
1424
+ let offset = 0;
1425
+ let expectedTotal: number | null = null;
1426
+
1427
+ while (true) {
1428
+ const page = await fetchPage(offset, CONTENT_DATABASE_LIST_PAGE_SIZE);
1429
+ const { pagination } = page;
1430
+ if (!pagination) {
1431
+ throw new Error(
1432
+ "list-content-databases returned no pagination boundary; refusing to treat the result as complete.",
1433
+ );
1434
+ }
1435
+ if (
1436
+ pagination.offset !== offset ||
1437
+ pagination.limit !== CONTENT_DATABASE_LIST_PAGE_SIZE ||
1438
+ pagination.returnedItems !== page.databases.length
1439
+ ) {
1440
+ throw new Error(
1441
+ "list-content-databases returned inconsistent pagination metadata; retry the complete read.",
1442
+ );
1443
+ }
1444
+ if (expectedTotal === null) expectedTotal = pagination.totalItems;
1445
+ if (pagination.totalItems !== expectedTotal) {
1446
+ throw new Error(
1447
+ "Content databases changed during paginated discovery; retry the complete read.",
1448
+ );
1449
+ }
1450
+ for (const database of page.databases) {
1451
+ if (databaseIds.has(database.databaseId)) {
1452
+ throw new Error(
1453
+ `list-content-databases repeated database "${database.databaseId}" across pages; refusing an ambiguous result.`,
1454
+ );
1455
+ }
1456
+ databaseIds.add(database.databaseId);
1457
+ databases.push(database);
1458
+ }
1459
+
1460
+ const expectedNextOffset = offset + page.databases.length;
1461
+ if (!pagination.hasMore) {
1462
+ if (
1463
+ pagination.nextOffset !== null ||
1464
+ expectedNextOffset !== expectedTotal ||
1465
+ databases.length !== expectedTotal
1466
+ ) {
1467
+ throw new Error(
1468
+ "list-content-databases claimed exhaustion before every declared database was returned.",
1469
+ );
1470
+ }
1471
+ return databases;
1472
+ }
1473
+ if (
1474
+ pagination.nextOffset !== expectedNextOffset ||
1475
+ pagination.nextOffset <= offset
1476
+ ) {
1477
+ throw new Error(
1478
+ "list-content-databases returned a non-advancing continuation; refusing a clipped result.",
1479
+ );
1480
+ }
1481
+ offset = pagination.nextOffset;
1482
+ }
1403
1483
  }
1404
1484
 
1405
1485
  export function useSuggestSourceJoinKey(args: {
@@ -6,7 +6,7 @@ This generated matrix tracks whether high-value Content UI operations use the sa
6
6
  | -------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | -------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------ |
7
7
  | comments.threads | comments | List, add, reply, resolve, reopen, and delete comment threads | action-backed | `add-comment`, `delete-comment`, `list-comments`, `update-comment` | `app/components/editor/CommentsSidebar.tsx`, `app/hooks/use-comments.ts` | Comment threads, replies, anchors, mentions, resolution state, and deletion are stored through comment actions. | - | - | P0 | seeded | - | - | - |
8
8
  | database.form-submissions | database | Submit public database forms as new rows | action-backed | `submit-content-database-form` | `app/components/editor/database/FormView.tsx` | A validated form submission atomically creates a database row document and its editable property values. | - | - | P0 | covered | `actions/submit-content-database-form.db.test.ts` | - | - |
9
- | database.lifecycle-and-trash | database | Create, soft-delete, restore, list, and inspect content databases | action-backed | `create-content-database`, `create-inline-content-database`, `delete-content-database`, `get-content-database`, `list-content-databases`, `list-trashed-content-databases`, `restore-content-database` | `app/components/editor/SlashCommandMenu.tsx`, `app/hooks/use-content-database.ts`, `app/hooks/use-documents.ts` | Database pages and database records are created, read, soft-deleted, restored, and listed. | - | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts` | `database-source-scope` | - |
9
+ | database.lifecycle-and-trash | database | Create, soft-delete, restore, list, and inspect content databases | action-backed | `create-content-database`, `create-inline-content-database`, `delete-content-database`, `describe-content-database`, `get-content-database`, `list-content-databases`, `list-trashed-content-databases`, `restore-content-database` | `app/components/editor/SlashCommandMenu.tsx`, `app/hooks/use-content-database.ts`, `app/hooks/use-documents.ts` | Database pages and database records are created, read, soft-deleted, restored, and listed. | - | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts`, `actions/list-content-databases.db.test.ts`, `server/plugins/agent-chat.spec.ts`, `../../packages/core/src/server/agent-chat/content-a2a-capabilities.spec.ts` | `database-source-scope` | - |
10
10
  | database.private-preview-drafts | database | Persist and reconcile a user's private database-page preview draft | action-backed | `get-preview-document-draft`, `update-preview-document-draft` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-documents.ts` | A user's private preview draft is read, saved, conflict-checked, or deleted without changing the shared database page until the normal save flow applies it. | These per-user editor-state actions are intentionally hidden from agent tools because preview drafts are a private UI recovery mechanism. | - | P1 | covered | `actions/preview-document-draft.db.test.ts` | - | - |
11
11
  | database.properties-and-view-config | database | Configure properties, values, ordering, and saved views | action-backed | `configure-document-property`, `delete-document-property`, `duplicate-document-property`, `get-content-database-personal-view`, `list-document-properties`, `reorder-document-property`, `set-document-property`, `update-content-database-personal-view`, `update-content-database-view` | `app/components/editor/DocumentProperties.tsx`, `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-document-properties.ts` | Property schemas, property values, property order, filters, sorts, grouping, hidden columns, view type, and view settings are stored. | - | - | P0 | covered | `actions/bind-content-database-source-field.db.test.ts`, `actions/content-database-source-actions.test.ts`, `actions/resync-content-database-source.db.test.ts` | `database-source-scope` | - |
12
12
  | database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `update-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `migrate-content-database-rows`, `move-database-item`, `set-document-property` | `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx` | Database row memberships and ordering are created, duplicated, moved, edited, and removed without deleting the backing page; bounded migrations atomically update row bodies and properties through the same canonical data model. | - | - | P0 | covered | `actions/database-row-batch-actions.db.test.ts`, `actions/upsert-database-item-by-key.db.test.ts`, `actions/migrate-content-database-rows.db.test.ts`, `parity/__tests__/database-row-batch-reliability.test.ts` | `database-bulk-row-reliability` | - |
@@ -275,6 +275,7 @@ export const parityMatrix: ParityRow[] = [
275
275
  "create-content-database",
276
276
  "create-inline-content-database",
277
277
  "delete-content-database",
278
+ "describe-content-database",
278
279
  "get-content-database",
279
280
  "list-content-databases",
280
281
  "list-trashed-content-databases",
@@ -285,7 +286,12 @@ export const parityMatrix: ParityRow[] = [
285
286
  spinePriority: "P0",
286
287
  testCoverage: "covered",
287
288
  followUpPR: null,
288
- coverageRefs: ["actions/content-database-lifecycle.db.test.ts"],
289
+ coverageRefs: [
290
+ "actions/content-database-lifecycle.db.test.ts",
291
+ "actions/list-content-databases.db.test.ts",
292
+ "server/plugins/agent-chat.spec.ts",
293
+ "../../packages/core/src/server/agent-chat/content-a2a-capabilities.spec.ts",
294
+ ],
289
295
  evalScenarioIds: ["database-source-scope"],
290
296
  },
291
297
  {
@@ -34,6 +34,9 @@ export default createAgentChatPlugin({
34
34
  durableBackgroundRuns: true,
35
35
  actions: loadActionsFromStaticRegistry(actionsRegistry),
36
36
  initialToolNames: INITIAL_TOOL_NAMES,
37
+ mcp: {
38
+ connectorCatalog: ["list-content-databases", "describe-content-database"],
39
+ },
37
40
  anonymousOwner: resolvePublicViewerOwner,
38
41
  extraContext: publicDocumentExtraContext,
39
42
  // Enable sandboxed JavaScript execution so Content agents can fetch,
@@ -1077,11 +1077,20 @@ export interface ChangeContentDatabaseSourceRoleRequest {
1077
1077
  export interface ContentDatabaseSummary {
1078
1078
  databaseId: string;
1079
1079
  documentId: string;
1080
+ spaceId: string | null;
1080
1081
  title: string;
1082
+ description: string;
1083
+ }
1084
+
1085
+ export interface ContentDatabaseDescriptionResponse {
1086
+ database: ContentDatabase;
1087
+ contextPath: ContentContextPathEntry[];
1088
+ properties: DocumentProperty[];
1081
1089
  }
1082
1090
 
1083
1091
  export interface ListContentDatabasesResponse {
1084
1092
  databases: ContentDatabaseSummary[];
1093
+ pagination: DocumentDiscoveryPagination;
1085
1094
  }
1086
1095
 
1087
1096
  export interface TrashedContentDatabaseSummary {
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;