@agent-native/core 0.149.5 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/content/actions/_document-discovery-query.ts +127 -0
- package/corpus/templates/content/actions/list-documents.ts +189 -198
- package/corpus/templates/content/actions/search-documents.ts +95 -24
- package/corpus/templates/content/app/hooks/use-documents.ts +84 -6
- package/corpus/templates/content/shared/api.ts +10 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
- package/package.json +1 -1
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
|
@@ -3,22 +3,21 @@ import {
|
|
|
3
3
|
getRequestOrgId,
|
|
4
4
|
getRequestUserEmail,
|
|
5
5
|
} from "@agent-native/core/server/request-context";
|
|
6
|
-
import {
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
300
|
-
.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 {
|
|
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 {
|
|
3
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
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
|
});
|