@agent-native/core 0.149.4 → 0.149.6

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 (58) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +5 -0
  3. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +5 -0
  4. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +5 -0
  5. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +5 -0
  6. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
  7. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +5 -0
  8. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +5 -0
  9. package/corpus/templates/content/actions/_document-discovery-query.ts +127 -0
  10. package/corpus/templates/content/actions/list-documents.ts +189 -198
  11. package/corpus/templates/content/actions/search-documents.ts +95 -24
  12. package/corpus/templates/content/app/hooks/use-documents.ts +84 -6
  13. package/corpus/templates/content/shared/api.ts +10 -0
  14. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +5 -0
  15. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +5 -0
  16. package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +5 -0
  17. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +5 -0
  18. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +5 -0
  19. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +5 -0
  20. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +5 -0
  21. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +5 -0
  22. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +23 -4
  23. package/corpus/templates/slides/actions/get-deck.ts +25 -9
  24. package/corpus/templates/slides/actions/patch-deck.ts +51 -0
  25. package/corpus/templates/slides/actions/update-slide.ts +144 -14
  26. package/corpus/templates/slides/app/lib/create-deck-generation.ts +12 -4
  27. package/corpus/templates/slides/changelog/2026-08-11-pdf-requests-to-restyle-and-preserve-a-source-deck-now-impor.md +6 -0
  28. package/corpus/templates/slides/changelog/2026-08-11-slide-edits-preserve-their-layout-without-introducing-hidden.md +6 -0
  29. package/corpus/templates/slides/changelog/2026-08-11-slides-reliably-imports-uploaded-pdfs-and-presentations.md +6 -0
  30. package/corpus/templates/slides/changelog/2026-08-11-slides-support-atomic-code-style-html-patches-without-regene.md +6 -0
  31. package/corpus/templates/slides/package.json +1 -0
  32. package/corpus/templates/slides/server/lib/slide-content-patch.ts +355 -0
  33. package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +28 -9
  34. package/corpus/templates/slides/server/plugins/agent-chat.ts +12 -3
  35. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +5 -0
  36. package/dist/cli/create.js +49 -0
  37. package/dist/client/chat-first/index.d.ts +1 -1
  38. package/dist/client/chat-first/index.js +1 -1
  39. package/dist/client/chat-first/primary-nav.d.ts +3 -1
  40. package/dist/client/chat-first/primary-nav.js +8 -2
  41. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  42. package/dist/notifications/routes.d.ts +3 -3
  43. package/dist/observability/routes.d.ts +3 -3
  44. package/dist/private-blob/registry.js +67 -4
  45. package/dist/resources/handlers.d.ts +1 -1
  46. package/dist/server/realtime-token.d.ts +1 -1
  47. package/dist/server/transcribe-voice.d.ts +1 -1
  48. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
  49. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
  50. package/dist/templates/default/pnpm-workspace.yaml +44 -0
  51. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
  52. package/dist/templates/workspace-root/pnpm-workspace.yaml +44 -0
  53. package/package.json +1 -1
  54. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
  55. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
  56. package/src/templates/default/pnpm-workspace.yaml +44 -0
  57. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
  58. package/src/templates/workspace-root/pnpm-workspace.yaml +44 -0
@@ -3,22 +3,21 @@ import {
3
3
  getRequestOrgId,
4
4
  getRequestUserEmail,
5
5
  } from "@agent-native/core/server/request-context";
6
- import {
7
- accessFilter,
8
- ROLE_RANK,
9
- type ShareRole,
10
- } from "@agent-native/core/sharing";
11
- import { and, asc, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
6
+ import { ROLE_RANK, type ShareRole } from "@agent-native/core/sharing";
7
+ import { and, asc, eq, inArray, isNull, or, sql } from "drizzle-orm";
12
8
  import { z } from "zod";
13
9
 
14
10
  import { getDb, schema } from "../server/db/index.js";
15
- import {
16
- documentDiscoveryFilter,
17
- parseDocumentHideFromSearch,
18
- } from "../server/lib/documents.js";
11
+ import { parseDocumentHideFromSearch } from "../server/lib/documents.js";
19
12
  import { favoriteDocumentIds } from "./_content-favorites.js";
20
13
  import { listContentOrganizationMemberships } from "./_content-space-access.js";
21
14
  import { serializeDatabaseMembership } from "./_database-utils.js";
15
+ import {
16
+ DOCUMENT_DISCOVERY_DEFAULT_LIMIT,
17
+ DOCUMENT_DISCOVERY_MAX_LIMIT,
18
+ documentDiscoveryPagination,
19
+ documentDiscoveryWhere,
20
+ } from "./_document-discovery-query.js";
22
21
  import { serializeDocumentSource } from "./_document-source.js";
23
22
  import { parseDatabaseViewConfig } from "./_property-utils.js";
24
23
 
@@ -45,10 +44,41 @@ function strongerRole(current: ShareRole | null, next: ShareRole): ShareRole {
45
44
 
46
45
  export default defineAction({
47
46
  description:
48
- "List document metadata ordered by position. Does not return full document bodies; use get-document for one document's content.",
49
- schema: z.object({}),
47
+ "List one bounded page of access-scoped document metadata ordered by position. Returns explicit pagination; follow nextOffset until hasMore is false. Does not return full document bodies; use get-document for one document's content.",
48
+ schema: z.object({
49
+ limit: z.coerce
50
+ .number()
51
+ .int()
52
+ .min(1)
53
+ .max(DOCUMENT_DISCOVERY_MAX_LIMIT)
54
+ .default(DOCUMENT_DISCOVERY_DEFAULT_LIMIT)
55
+ .describe("Maximum documents returned in this page"),
56
+ offset: z.coerce
57
+ .number()
58
+ .int()
59
+ .min(0)
60
+ .default(0)
61
+ .describe("Zero-based continuation offset"),
62
+ exactTitle: z
63
+ .string()
64
+ .trim()
65
+ .min(1)
66
+ .optional()
67
+ .describe("Case-sensitive exact document title"),
68
+ parentId: z
69
+ .string()
70
+ .nullable()
71
+ .optional()
72
+ .describe("Exact parent document ID; null selects roots"),
73
+ spaceId: z.string().min(1).optional().describe("Exact Content space ID"),
74
+ documentType: z
75
+ .enum(["page", "database"])
76
+ .optional()
77
+ .describe("Only ordinary pages or database pages"),
78
+ }),
50
79
  http: { method: "GET" },
51
- run: async () => {
80
+ readOnly: true,
81
+ run: async (args) => {
52
82
  const db = getDb();
53
83
  const userEmail = getRequestUserEmail();
54
84
  const activeOrgId = getRequestOrgId();
@@ -61,13 +91,19 @@ export default defineAction({
61
91
  ...(!userEmail && activeOrgId ? [activeOrgId] : []),
62
92
  ]),
63
93
  ];
64
- const accessContexts = [
65
- { userEmail: userEmail ?? undefined },
66
- ...authorizedOrgIds.map((orgId) => ({
67
- userEmail: userEmail ?? undefined,
68
- orgId,
69
- })),
70
- ];
94
+ const where = documentDiscoveryWhere({
95
+ userEmail,
96
+ authorizedOrgIds,
97
+ exactTitle: args.exactTitle,
98
+ parentId: args.parentId,
99
+ spaceId: args.spaceId,
100
+ documentType: args.documentType,
101
+ });
102
+ const [countRow] = await db
103
+ .select({ count: sql<number>`count(*)` })
104
+ .from(schema.documents)
105
+ .where(where);
106
+ const totalItems = Number(countRow?.count ?? 0);
71
107
  // Projection that deliberately avoids pulling the full `content` blob:
72
108
  // document bodies can be multi-MB, and the list/tree path only needs a
73
109
  // short preview plus the true length. `substr` truncates the transferred
@@ -99,21 +135,10 @@ export default defineAction({
99
135
  updatedAt: schema.documents.updatedAt,
100
136
  })
101
137
  .from(schema.documents)
102
- .where(
103
- and(
104
- or(
105
- ...accessContexts.map((context) =>
106
- accessFilter(schema.documents, schema.documentShares, context),
107
- ),
108
- ),
109
- isNull(schema.documents.trashedAt),
110
- documentDiscoveryFilter({
111
- userEmail,
112
- orgIds: authorizedOrgIds,
113
- }),
114
- ),
115
- )
116
- .orderBy(asc(schema.documents.position));
138
+ .where(where)
139
+ .orderBy(asc(schema.documents.position), asc(schema.documents.id))
140
+ .limit(args.limit)
141
+ .offset(args.offset);
117
142
 
118
143
  const shareRoleByDocumentId = new Map<string, ShareRole>();
119
144
  const notionPageIdByDocumentId = new Map<string, string>();
@@ -128,7 +153,6 @@ export default defineAction({
128
153
  database: typeof schema.contentDatabases.$inferSelect;
129
154
  }
130
155
  >();
131
- const softDeletedDocumentIds = new Set<string>();
132
156
  const favoriteIds = userEmail
133
157
  ? await favoriteDocumentIds(
134
158
  db,
@@ -161,89 +185,75 @@ export default defineAction({
161
185
  // These queries all depend only on the initial `documents` id list
162
186
  // (already fetched above), not on each other's results, so they run
163
187
  // concurrently instead of as sequential round-trips.
164
- const [
165
- notionLinks,
166
- shareRows,
167
- databases,
168
- databaseMemberships,
169
- softDeletedDatabases,
170
- ] = await Promise.all([
171
- db
172
- .select({
173
- documentId: schema.documentSyncLinks.documentId,
174
- remotePageId: schema.documentSyncLinks.remotePageId,
175
- })
176
- .from(schema.documentSyncLinks)
177
- .where(
178
- inArray(schema.documentSyncLinks.documentId, visibleDocumentIds),
179
- ),
180
- principalClauses.length > 0
181
- ? db
182
- .select({
183
- resourceId: schema.documentShares.resourceId,
184
- role: schema.documentShares.role,
185
- })
186
- .from(schema.documentShares)
187
- .where(
188
- and(
189
- inArray(schema.documentShares.resourceId, visibleDocumentIds),
190
- or(...principalClauses),
191
- ),
192
- )
193
- : Promise.resolve([] as { resourceId: string; role: ShareRole }[]),
194
- db
195
- .select()
196
- .from(schema.contentDatabases)
197
- .where(
198
- and(
199
- inArray(schema.contentDatabases.documentId, visibleDocumentIds),
200
- isNull(schema.contentDatabases.deletedAt),
201
- ),
202
- )
203
- .orderBy(
204
- sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
205
- sql`CASE WHEN ${schema.contentDatabases.systemRole} = 'files' THEN 0 ELSE 1 END`,
206
- asc(schema.contentDatabases.id),
207
- ),
208
- db
209
- .select({
210
- item: schema.contentDatabaseItems,
211
- database: schema.contentDatabases,
212
- })
213
- .from(schema.contentDatabaseItems)
214
- .innerJoin(
215
- schema.contentDatabases,
216
- eq(
217
- schema.contentDatabases.id,
218
- schema.contentDatabaseItems.databaseId,
188
+ const [notionLinks, shareRows, databases, databaseMemberships] =
189
+ await Promise.all([
190
+ db
191
+ .select({
192
+ documentId: schema.documentSyncLinks.documentId,
193
+ remotePageId: schema.documentSyncLinks.remotePageId,
194
+ })
195
+ .from(schema.documentSyncLinks)
196
+ .where(
197
+ inArray(schema.documentSyncLinks.documentId, visibleDocumentIds),
219
198
  ),
220
- )
221
- .where(
222
- and(
223
- inArray(
224
- schema.contentDatabaseItems.documentId,
225
- visibleDocumentIds,
199
+ principalClauses.length > 0
200
+ ? db
201
+ .select({
202
+ resourceId: schema.documentShares.resourceId,
203
+ role: schema.documentShares.role,
204
+ })
205
+ .from(schema.documentShares)
206
+ .where(
207
+ and(
208
+ inArray(
209
+ schema.documentShares.resourceId,
210
+ visibleDocumentIds,
211
+ ),
212
+ or(...principalClauses),
213
+ ),
214
+ )
215
+ : Promise.resolve([] as { resourceId: string; role: ShareRole }[]),
216
+ db
217
+ .select()
218
+ .from(schema.contentDatabases)
219
+ .where(
220
+ and(
221
+ inArray(schema.contentDatabases.documentId, visibleDocumentIds),
222
+ isNull(schema.contentDatabases.deletedAt),
226
223
  ),
227
- isNull(schema.contentDatabases.deletedAt),
224
+ )
225
+ .orderBy(
226
+ sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
227
+ sql`CASE WHEN ${schema.contentDatabases.systemRole} = 'files' THEN 0 ELSE 1 END`,
228
+ asc(schema.contentDatabases.id),
228
229
  ),
229
- )
230
- .orderBy(
231
- sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
232
- asc(schema.contentDatabases.id),
233
- ),
234
- db
235
- .select({
236
- id: schema.contentDatabases.id,
237
- documentId: schema.contentDatabases.documentId,
238
- })
239
- .from(schema.contentDatabases)
240
- .where(
241
- and(
242
- inArray(schema.contentDatabases.documentId, visibleDocumentIds),
243
- isNotNull(schema.contentDatabases.deletedAt),
230
+ db
231
+ .select({
232
+ item: schema.contentDatabaseItems,
233
+ database: schema.contentDatabases,
234
+ })
235
+ .from(schema.contentDatabaseItems)
236
+ .innerJoin(
237
+ schema.contentDatabases,
238
+ eq(
239
+ schema.contentDatabases.id,
240
+ schema.contentDatabaseItems.databaseId,
241
+ ),
242
+ )
243
+ .where(
244
+ and(
245
+ inArray(
246
+ schema.contentDatabaseItems.documentId,
247
+ visibleDocumentIds,
248
+ ),
249
+ isNull(schema.contentDatabases.deletedAt),
250
+ ),
251
+ )
252
+ .orderBy(
253
+ sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
254
+ asc(schema.contentDatabases.id),
244
255
  ),
245
- ),
246
- ]);
256
+ ]);
247
257
 
248
258
  for (const link of notionLinks) {
249
259
  notionPageIdByDocumentId.set(link.documentId, link.remotePageId);
@@ -268,93 +278,74 @@ export default defineAction({
268
278
  databaseMembershipByDocumentId.set(row.item.documentId, row);
269
279
  }
270
280
  }
271
-
272
- for (const database of softDeletedDatabases) {
273
- softDeletedDocumentIds.add(database.documentId);
274
- }
275
-
276
- if (softDeletedDatabases.length > 0) {
277
- const softDeletedItems = await db
278
- .select({ documentId: schema.contentDatabaseItems.documentId })
279
- .from(schema.contentDatabaseItems)
280
- .where(
281
- and(
282
- inArray(
283
- schema.contentDatabaseItems.databaseId,
284
- softDeletedDatabases.map((database) => database.id),
285
- ),
286
- inArray(
287
- schema.contentDatabaseItems.documentId,
288
- visibleDocumentIds,
289
- ),
290
- ),
291
- );
292
- for (const item of softDeletedItems) {
293
- softDeletedDocumentIds.add(item.documentId);
294
- }
295
- }
296
281
  }
297
282
 
298
- const mapped = documents
299
- .filter((d) => !softDeletedDocumentIds.has(d.id))
300
- .map((d) => {
301
- let accessRole: EffectiveRole = "viewer";
302
- const shareRole = shareRoleByDocumentId.get(d.id) ?? null;
303
- const database = databaseByDocumentId.get(d.id) ?? null;
304
- const databaseMembership =
305
- databaseMembershipByDocumentId.get(d.id) ?? null;
283
+ const mapped = documents.map((d) => {
284
+ let accessRole: EffectiveRole = "viewer";
285
+ const shareRole = shareRoleByDocumentId.get(d.id) ?? null;
286
+ const database = databaseByDocumentId.get(d.id) ?? null;
287
+ const databaseMembership =
288
+ databaseMembershipByDocumentId.get(d.id) ?? null;
306
289
 
307
- if (shareRole && ROLE_RANK[shareRole] > ROLE_RANK[accessRole]) {
308
- accessRole = shareRole;
309
- }
310
- if (
311
- userEmail &&
312
- d.ownerEmail === userEmail &&
313
- (!d.orgId || authorizedOrgIds.includes(d.orgId))
314
- ) {
315
- accessRole = "owner";
316
- }
290
+ if (shareRole && ROLE_RANK[shareRole] > ROLE_RANK[accessRole]) {
291
+ accessRole = shareRole;
292
+ }
293
+ if (
294
+ userEmail &&
295
+ d.ownerEmail === userEmail &&
296
+ (!d.orgId || authorizedOrgIds.includes(d.orgId))
297
+ ) {
298
+ accessRole = "owner";
299
+ }
317
300
 
318
- return {
319
- id: d.id,
320
- parentId: d.parentId,
321
- title: d.title,
322
- description: d.description,
323
- contentPreview: contentPreview(d.contentSnippet),
324
- contentLength: Number(d.contentLength) || 0,
325
- icon: d.icon,
326
- position: d.position,
327
- isFavorite: favoriteIds.has(d.id),
328
- hideFromSearch: parseDocumentHideFromSearch(d.hideFromSearch),
329
- notionPageId: notionPageIdByDocumentId.get(d.id) ?? null,
330
- notionPageUrl: notionPageIdByDocumentId.has(d.id)
331
- ? `https://www.notion.so/${notionPageIdByDocumentId.get(d.id)!.replace(/-/g, "")}`
332
- : null,
333
- visibility: d.visibility,
334
- source: serializeDocumentSource(d),
335
- database: database
336
- ? {
337
- id: database.id,
338
- documentId: database.documentId,
339
- title: database.title,
340
- systemRole: database.systemRole,
341
- description: d.description,
342
- viewConfig: parseDatabaseViewConfig(database.viewConfigJson),
343
- createdAt: database.createdAt,
344
- updatedAt: database.updatedAt,
345
- }
346
- : undefined,
347
- databaseMembership: databaseMembership
348
- ? serializeDatabaseMembership(databaseMembership)
349
- : undefined,
350
- accessRole,
351
- canEdit: canEditRole(accessRole),
352
- canManage: canManageRole(accessRole),
353
- createdAt: d.createdAt,
354
- updatedAt: d.updatedAt,
355
- };
356
- });
301
+ return {
302
+ id: d.id,
303
+ parentId: d.parentId,
304
+ title: d.title,
305
+ description: d.description,
306
+ contentPreview: contentPreview(d.contentSnippet),
307
+ contentLength: Number(d.contentLength) || 0,
308
+ icon: d.icon,
309
+ position: d.position,
310
+ isFavorite: favoriteIds.has(d.id),
311
+ hideFromSearch: parseDocumentHideFromSearch(d.hideFromSearch),
312
+ notionPageId: notionPageIdByDocumentId.get(d.id) ?? null,
313
+ notionPageUrl: notionPageIdByDocumentId.has(d.id)
314
+ ? `https://www.notion.so/${notionPageIdByDocumentId.get(d.id)!.replace(/-/g, "")}`
315
+ : null,
316
+ visibility: d.visibility,
317
+ source: serializeDocumentSource(d),
318
+ database: database
319
+ ? {
320
+ id: database.id,
321
+ documentId: database.documentId,
322
+ title: database.title,
323
+ systemRole: database.systemRole,
324
+ description: d.description,
325
+ viewConfig: parseDatabaseViewConfig(database.viewConfigJson),
326
+ createdAt: database.createdAt,
327
+ updatedAt: database.updatedAt,
328
+ }
329
+ : undefined,
330
+ databaseMembership: databaseMembership
331
+ ? serializeDatabaseMembership(databaseMembership)
332
+ : undefined,
333
+ accessRole,
334
+ canEdit: canEditRole(accessRole),
335
+ canManage: canManageRole(accessRole),
336
+ createdAt: d.createdAt,
337
+ updatedAt: d.updatedAt,
338
+ };
339
+ });
357
340
 
358
- return { documents: mapped };
341
+ return {
342
+ documents: mapped,
343
+ pagination: documentDiscoveryPagination({
344
+ offset: args.offset,
345
+ limit: args.limit,
346
+ totalItems,
347
+ returnedItems: mapped.length,
348
+ }),
349
+ };
359
350
  },
360
351
  });
@@ -1,13 +1,20 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, isNull, sql } from "drizzle-orm";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server/request-context";
6
+ import { asc, desc, sql } from "drizzle-orm";
4
7
  import { z } from "zod";
5
8
 
6
9
  import { getDb, schema } from "../server/db/index.js";
10
+ import { parseDocumentHideFromSearch } from "../server/lib/documents.js";
11
+ import { listContentOrganizationMemberships } from "./_content-space-access.js";
7
12
  import {
8
- documentDiscoveryFilter,
9
- parseDocumentHideFromSearch,
10
- } from "../server/lib/documents.js";
13
+ DOCUMENT_DISCOVERY_DEFAULT_LIMIT,
14
+ DOCUMENT_DISCOVERY_MAX_LIMIT,
15
+ documentDiscoveryPagination,
16
+ documentDiscoveryWhere,
17
+ } from "./_document-discovery-query.js";
11
18
 
12
19
  function escapeLike(s: string): string {
13
20
  return s.replace(/([\\%_])/g, "\\$1");
@@ -37,17 +44,78 @@ function makeSnippet(content: string, query: string, radius = 120) {
37
44
 
38
45
  export default defineAction({
39
46
  description:
40
- "Search documents by title and content. Returns metadata and snippets; use get-document for full content.",
41
- schema: z.object({
42
- query: z.string().describe("Search text"),
43
- limit: z.coerce.number().int().min(1).max(200).default(50),
44
- }),
47
+ "Search one bounded page of access-scoped documents by title and content, or find an exact title within a parent, space, and document type. Returns explicit pagination; follow nextOffset until hasMore is false. Returns metadata and snippets; use get-document for full content.",
48
+ schema: z
49
+ .object({
50
+ query: z.string().trim().min(1).optional().describe("Search text"),
51
+ exactTitle: z
52
+ .string()
53
+ .trim()
54
+ .min(1)
55
+ .optional()
56
+ .describe("Case-sensitive exact document title"),
57
+ parentId: z
58
+ .string()
59
+ .nullable()
60
+ .optional()
61
+ .describe("Exact parent document ID; null selects roots"),
62
+ spaceId: z.string().min(1).optional().describe("Exact Content space ID"),
63
+ documentType: z
64
+ .enum(["page", "database"])
65
+ .optional()
66
+ .describe("Only ordinary pages or database pages"),
67
+ limit: z.coerce
68
+ .number()
69
+ .int()
70
+ .min(1)
71
+ .max(DOCUMENT_DISCOVERY_MAX_LIMIT)
72
+ .default(DOCUMENT_DISCOVERY_DEFAULT_LIMIT)
73
+ .describe("Maximum documents returned in this page"),
74
+ offset: z.coerce
75
+ .number()
76
+ .int()
77
+ .min(0)
78
+ .default(0)
79
+ .describe("Zero-based continuation offset"),
80
+ })
81
+ .refine(
82
+ (args) => args.query !== undefined || args.exactTitle !== undefined,
83
+ {
84
+ message: "Provide query or exactTitle.",
85
+ },
86
+ ),
45
87
  http: { method: "GET" },
88
+ readOnly: true,
46
89
  run: async (args) => {
47
- const query = args.query;
48
-
49
90
  const db = getDb();
50
- const pattern = `%${escapeLike(query)}%`;
91
+ const userEmail = getRequestUserEmail();
92
+ const activeOrgId = getRequestOrgId();
93
+ const memberships = userEmail
94
+ ? await listContentOrganizationMemberships(userEmail)
95
+ : [];
96
+ const authorizedOrgIds = [
97
+ ...new Set([
98
+ ...memberships.map((membership) => membership.orgId),
99
+ ...(!userEmail && activeOrgId ? [activeOrgId] : []),
100
+ ]),
101
+ ];
102
+ const pattern = args.query ? `%${escapeLike(args.query)}%` : undefined;
103
+ const where = documentDiscoveryWhere({
104
+ userEmail,
105
+ authorizedOrgIds,
106
+ exactTitle: args.exactTitle,
107
+ parentId: args.parentId,
108
+ spaceId: args.spaceId,
109
+ documentType: args.documentType,
110
+ additional: pattern
111
+ ? sql`(${schema.documents.title} LIKE ${pattern} ESCAPE '\\' OR ${schema.documents.description} LIKE ${pattern} ESCAPE '\\' OR ${schema.documents.content} LIKE ${pattern} ESCAPE '\\')`
112
+ : undefined,
113
+ });
114
+ const [countRow] = await db
115
+ .select({ count: sql<number>`count(*)` })
116
+ .from(schema.documents)
117
+ .where(where);
118
+ const totalItems = Number(countRow?.count ?? 0);
51
119
 
52
120
  // Project a bounded preview of `content` instead of the full column:
53
121
  // document bodies can be multi-MB, and this action only returns a short
@@ -71,16 +139,10 @@ export default defineAction({
71
139
  updatedAt: schema.documents.updatedAt,
72
140
  })
73
141
  .from(schema.documents)
74
- .where(
75
- and(
76
- accessFilter(schema.documents, schema.documentShares),
77
- isNull(schema.documents.trashedAt),
78
- documentDiscoveryFilter(),
79
- sql`(${schema.documents.title} LIKE ${pattern} ESCAPE '\\' OR ${schema.documents.description} LIKE ${pattern} ESCAPE '\\' OR ${schema.documents.content} LIKE ${pattern} ESCAPE '\\')`,
80
- ),
81
- )
82
- .orderBy(sql`${schema.documents.updatedAt} DESC`)
83
- .limit(args.limit);
142
+ .where(where)
143
+ .orderBy(desc(schema.documents.updatedAt), asc(schema.documents.id))
144
+ .limit(args.limit)
145
+ .offset(args.offset);
84
146
 
85
147
  return {
86
148
  documents: docs.map((doc) => ({
@@ -89,11 +151,20 @@ export default defineAction({
89
151
  title: doc.title,
90
152
  description: doc.description,
91
153
  icon: doc.icon,
92
- snippet: makeSnippet(doc.contentPreview, query),
154
+ snippet: makeSnippet(
155
+ doc.contentPreview,
156
+ args.query ?? args.exactTitle ?? "",
157
+ ),
93
158
  contentLength: Number(doc.contentLength) || 0,
94
159
  hideFromSearch: parseDocumentHideFromSearch(doc.hideFromSearch),
95
160
  updatedAt: doc.updatedAt,
96
161
  })),
162
+ pagination: documentDiscoveryPagination({
163
+ offset: args.offset,
164
+ limit: args.limit,
165
+ totalItems,
166
+ returnedItems: docs.length,
167
+ }),
97
168
  };
98
169
  },
99
170
  });