@promptowl/contextnest-community 1.16.1 → 1.17.0
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/CONFIGURATION.md +415 -415
- package/LICENSE.md +142 -142
- package/README.md +164 -169
- package/dist/{chunk-QJVRZT6M.js → chunk-4YGJSUJC.js} +35 -5
- package/dist/{chunk-OTGZQZKK.js → chunk-HWDPCPYT.js} +10 -9
- package/dist/{chunk-L7UCMGWZ.js → chunk-SNFGU5Q7.js} +166 -81
- package/dist/{chunk-D6HICFVW.js → chunk-UXJSO54R.js} +1 -1
- package/dist/{chunk-CLKZZZS2.js → chunk-YF4OFT2V.js} +1 -1
- package/dist/{grants-service-I535QVTW.js → grants-service-4NXPBYWD.js} +3 -3
- package/dist/index.js +213 -65
- package/dist/{review-service-V2GR6VM3.js → review-service-3CCKDO5N.js} +9 -7
- package/dist/{stewardship-service-LTREGVSJ.js → stewardship-service-OHTTZFND.js} +2 -2
- package/dist/{version-service-V6K5SZ3P.js → version-service-EFZGGL3Y.js} +2 -2
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-D1299AIf.css +1 -0
- package/dist/web3/assets/index-W6tEA3RK.js +1046 -0
- package/dist/web3/index.html +14 -14
- package/package.json +165 -165
- package/dist/web3/assets/index-DnQARUL3.css +0 -1
- package/dist/web3/assets/index-_m7WyozM.js +0 -1051
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
addWatcher,
|
|
4
4
|
approve,
|
|
5
5
|
canReadNode,
|
|
6
|
+
canRequestDeletion,
|
|
6
7
|
cancelReview,
|
|
7
8
|
declineDeletion,
|
|
8
9
|
deletionTargetTitle,
|
|
@@ -38,17 +39,18 @@ import {
|
|
|
38
39
|
seedDefaultEdgeTypes,
|
|
39
40
|
sendTestNotification,
|
|
40
41
|
submitForReview,
|
|
41
|
-
withNodeWriteLock
|
|
42
|
-
|
|
42
|
+
withNodeWriteLock,
|
|
43
|
+
withdrawDeletionRequest
|
|
44
|
+
} from "./chunk-4YGJSUJC.js";
|
|
43
45
|
import {
|
|
44
46
|
createGrant,
|
|
45
47
|
deleteGrant,
|
|
46
48
|
grantCoversNode,
|
|
47
|
-
|
|
49
|
+
grantRoleForNode,
|
|
48
50
|
listGrants,
|
|
49
51
|
listUserGrants,
|
|
50
52
|
resolveNodeGrant
|
|
51
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-HWDPCPYT.js";
|
|
52
54
|
import {
|
|
53
55
|
generateApiKey,
|
|
54
56
|
getKeyPrefix,
|
|
@@ -69,7 +71,7 @@ import {
|
|
|
69
71
|
listMyDrafts,
|
|
70
72
|
setApprovedVersion,
|
|
71
73
|
upsertVersion
|
|
72
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-UXJSO54R.js";
|
|
73
75
|
import {
|
|
74
76
|
canCreateInNest,
|
|
75
77
|
canManageStewards,
|
|
@@ -89,7 +91,7 @@ import {
|
|
|
89
91
|
resolveUserRoles,
|
|
90
92
|
syncFromConfig,
|
|
91
93
|
updateSteward
|
|
92
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-YF4OFT2V.js";
|
|
93
95
|
import {
|
|
94
96
|
addMember,
|
|
95
97
|
buildTitleMap,
|
|
@@ -145,6 +147,7 @@ import {
|
|
|
145
147
|
renameTeam,
|
|
146
148
|
replaceTeamRoster,
|
|
147
149
|
resolveMemberIdentity,
|
|
150
|
+
resolveNestAccess,
|
|
148
151
|
resolveNestPath,
|
|
149
152
|
resolveNestPermission,
|
|
150
153
|
sendEmailToRecipient,
|
|
@@ -162,7 +165,7 @@ import {
|
|
|
162
165
|
updateMemberRole,
|
|
163
166
|
validateLicense,
|
|
164
167
|
verifySmtp
|
|
165
|
-
} from "./chunk-
|
|
168
|
+
} from "./chunk-SNFGU5Q7.js";
|
|
166
169
|
import {
|
|
167
170
|
AppError,
|
|
168
171
|
ConflictError,
|
|
@@ -1863,7 +1866,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
|
|
|
1863
1866
|
async function mirrorVersion(input) {
|
|
1864
1867
|
const { storage } = await engineCache.get(input.nestId);
|
|
1865
1868
|
const node = await storage.readDocument(input.documentId);
|
|
1866
|
-
const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-
|
|
1869
|
+
const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-EFZGGL3Y.js");
|
|
1867
1870
|
await upsertVersion2({
|
|
1868
1871
|
nestId: input.nestId,
|
|
1869
1872
|
nodeId: input.documentId,
|
|
@@ -2199,10 +2202,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2199
2202
|
nodes,
|
|
2200
2203
|
approvedVersions ?? void 0
|
|
2201
2204
|
);
|
|
2202
|
-
|
|
2203
|
-
const grants = await listUserGrants(nestId, userId);
|
|
2204
|
-
accessible = grants.length ? nodes.filter((d) => grantCoversNode(grants, d.id)) : accessible;
|
|
2205
|
-
}
|
|
2205
|
+
const myGrants = await listUserGrants(nestId, userId);
|
|
2206
2206
|
if (approvedVersions && !publicReader) {
|
|
2207
2207
|
accessible = accessible.filter((d) => approvedVersions.has(d.id));
|
|
2208
2208
|
}
|
|
@@ -2222,6 +2222,9 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2222
2222
|
r.author = people.author;
|
|
2223
2223
|
r.collaborators = people.collaborators;
|
|
2224
2224
|
}
|
|
2225
|
+
if (myGrants.length) {
|
|
2226
|
+
r.myGrant = grantRoleForNode(myGrants, doc.id);
|
|
2227
|
+
}
|
|
2225
2228
|
if (approvedVersions) {
|
|
2226
2229
|
const approved = approvedVersions.get(doc.id);
|
|
2227
2230
|
if (approved != null) {
|
|
@@ -3099,16 +3102,22 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
3099
3102
|
}
|
|
3100
3103
|
|
|
3101
3104
|
// src/nests/routes.ts
|
|
3102
|
-
async function
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
"SELECT user_id FROM nests WHERE id = ?",
|
|
3107
|
-
[nestId]
|
|
3108
|
-
);
|
|
3109
|
-
if (nest && nest.user_id === ANON_USER_ID) return "owner";
|
|
3105
|
+
async function effectiveAccess(nestId, userId) {
|
|
3106
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
3107
|
+
if (config.AUTH_MODE === "open" && access.ownerId === ANON_USER_ID) {
|
|
3108
|
+
return { permission: "owner", access };
|
|
3110
3109
|
}
|
|
3111
|
-
|
|
3110
|
+
const permission = access.permission;
|
|
3111
|
+
if (permission === "none" && access.hasGrant) {
|
|
3112
|
+
return { permission: "read", access };
|
|
3113
|
+
}
|
|
3114
|
+
return { permission, access };
|
|
3115
|
+
}
|
|
3116
|
+
async function effectivePermission(nestId, userId) {
|
|
3117
|
+
return (await effectiveAccess(nestId, userId)).permission;
|
|
3118
|
+
}
|
|
3119
|
+
function isGrantOnly(access) {
|
|
3120
|
+
return access.permission === "none" && access.hasGrant;
|
|
3112
3121
|
}
|
|
3113
3122
|
var nestRoutes = new Hono3();
|
|
3114
3123
|
nestRoutes.get("/", async (c) => {
|
|
@@ -3119,8 +3128,31 @@ nestRoutes.get("/", async (c) => {
|
|
|
3119
3128
|
const db = getDb();
|
|
3120
3129
|
const ownerEmailSql = "SELECT email FROM users WHERE id = ?";
|
|
3121
3130
|
const callerEmail = await resolveCallerEmail(userId);
|
|
3131
|
+
const myGrantRows = await db.all(
|
|
3132
|
+
"SELECT nest_id, target FROM grants WHERE user_id = ?",
|
|
3133
|
+
[userId]
|
|
3134
|
+
);
|
|
3135
|
+
const grantTargetsByNest = /* @__PURE__ */ new Map();
|
|
3136
|
+
for (const r of myGrantRows) {
|
|
3137
|
+
const arr = grantTargetsByNest.get(r.nest_id) ?? [];
|
|
3138
|
+
arr.push({ target: r.target });
|
|
3139
|
+
grantTargetsByNest.set(r.nest_id, arr);
|
|
3140
|
+
}
|
|
3122
3141
|
const annotate = async (n) => {
|
|
3123
|
-
const permission = await
|
|
3142
|
+
const { permission, access } = await effectiveAccess(n.id, userId);
|
|
3143
|
+
if (permission !== "owner" && // open-mode commons promotion outranks a grant
|
|
3144
|
+
isGrantOnly(access)) {
|
|
3145
|
+
const owner_email2 = n.user_id === ANON_USER_ID ? null : (await db.get(ownerEmailSql, [n.user_id]))?.email ?? null;
|
|
3146
|
+
return {
|
|
3147
|
+
...n,
|
|
3148
|
+
permission: "read",
|
|
3149
|
+
is_owner: false,
|
|
3150
|
+
owner_email: owner_email2,
|
|
3151
|
+
roles: ["viewer"],
|
|
3152
|
+
grant_only: true,
|
|
3153
|
+
stewardship_enabled: !!n.stewardship_enabled
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3124
3156
|
const is_owner = permission === "owner";
|
|
3125
3157
|
const needOwnerEmail = !is_owner && n.user_id !== ANON_USER_ID;
|
|
3126
3158
|
const [roles, owner_email] = await Promise.all([
|
|
@@ -3135,6 +3167,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
3135
3167
|
is_owner,
|
|
3136
3168
|
owner_email,
|
|
3137
3169
|
roles,
|
|
3170
|
+
grant_only: false,
|
|
3138
3171
|
stewardship_enabled: !!n.stewardship_enabled
|
|
3139
3172
|
};
|
|
3140
3173
|
};
|
|
@@ -3178,14 +3211,38 @@ nestRoutes.get("/", async (c) => {
|
|
|
3178
3211
|
if (arr.length < 8) arr.push(r.email);
|
|
3179
3212
|
collabEmailsByNest.set(r.nest_id, arr);
|
|
3180
3213
|
}
|
|
3181
|
-
const
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3214
|
+
const grantOnlyIds = out.filter((n) => n.grant_only).map((n) => n.id);
|
|
3215
|
+
const grantedDocsByNest = /* @__PURE__ */ new Map();
|
|
3216
|
+
if (grantOnlyIds.length) {
|
|
3217
|
+
const rows = await db.all(
|
|
3218
|
+
`SELECT DISTINCT nest_id, node_id FROM node_versions
|
|
3219
|
+
WHERE nest_id IN (${grantOnlyIds.map(() => "?").join(",")})`,
|
|
3220
|
+
grantOnlyIds
|
|
3221
|
+
);
|
|
3222
|
+
for (const r of rows) {
|
|
3223
|
+
const targets = grantTargetsByNest.get(r.nest_id) ?? [];
|
|
3224
|
+
if (grantCoversNode(targets, r.node_id)) {
|
|
3225
|
+
grantedDocsByNest.set(r.nest_id, (grantedDocsByNest.get(r.nest_id) ?? 0) + 1);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
const withMeta = out.map(
|
|
3230
|
+
(n) => n.grant_only ? {
|
|
3231
|
+
...n,
|
|
3232
|
+
document_count: grantedDocsByNest.get(n.id) ?? 0,
|
|
3233
|
+
collaborator_count: 0,
|
|
3234
|
+
steward_count: 0,
|
|
3235
|
+
steward_emails: [],
|
|
3236
|
+
collaborator_emails: []
|
|
3237
|
+
} : {
|
|
3238
|
+
...n,
|
|
3239
|
+
document_count: docByNest.get(n.id) ?? 0,
|
|
3240
|
+
collaborator_count: collabByNest.get(n.id) ?? 0,
|
|
3241
|
+
steward_count: stewardByNest.get(n.id) ?? 0,
|
|
3242
|
+
steward_emails: stewardEmailsByNest.get(n.id) ?? [],
|
|
3243
|
+
collaborator_emails: collabEmailsByNest.get(n.id) ?? []
|
|
3244
|
+
}
|
|
3245
|
+
);
|
|
3189
3246
|
return c.json({ nests: withMeta });
|
|
3190
3247
|
});
|
|
3191
3248
|
nestRoutes.post("/", async (c) => {
|
|
@@ -3291,7 +3348,7 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
3291
3348
|
const nestId = c.req.param("nestId");
|
|
3292
3349
|
if (nestId === "unsynced") return handleUnsyncedList(c);
|
|
3293
3350
|
const userId = c.get("userId");
|
|
3294
|
-
const permission = await
|
|
3351
|
+
const { permission, access } = await effectiveAccess(nestId, userId);
|
|
3295
3352
|
if (permission === "none") {
|
|
3296
3353
|
throw new NotFoundError("Nest not found");
|
|
3297
3354
|
}
|
|
@@ -3301,8 +3358,15 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
3301
3358
|
roles = ["owner", ...roles];
|
|
3302
3359
|
}
|
|
3303
3360
|
const myStewards = await getStewardsForUser(nestId, email);
|
|
3361
|
+
const grant_only = roles.length === 0 && isGrantOnly(access);
|
|
3304
3362
|
const nest = await getNest(nestId);
|
|
3305
|
-
return c.json({
|
|
3363
|
+
return c.json({
|
|
3364
|
+
nest,
|
|
3365
|
+
permission,
|
|
3366
|
+
roles: grant_only ? ["viewer"] : roles,
|
|
3367
|
+
myStewards,
|
|
3368
|
+
grant_only
|
|
3369
|
+
});
|
|
3306
3370
|
});
|
|
3307
3371
|
nestRoutes.patch("/:nestId", async (c) => {
|
|
3308
3372
|
const nestId = c.req.param("nestId");
|
|
@@ -4002,6 +4066,25 @@ teamRoutes.delete("/:teamId/members/:userId", async (c) => {
|
|
|
4002
4066
|
});
|
|
4003
4067
|
|
|
4004
4068
|
// src/nodes/search.ts
|
|
4069
|
+
async function discoverForSearch(storage) {
|
|
4070
|
+
return storage.discoverDocuments({ includeRetired: true });
|
|
4071
|
+
}
|
|
4072
|
+
function titleScore(title, query) {
|
|
4073
|
+
const t = (title || "").toLowerCase();
|
|
4074
|
+
const q = query.trim().toLowerCase();
|
|
4075
|
+
if (!q) return 0;
|
|
4076
|
+
if (t === q) return 4;
|
|
4077
|
+
let midWord = false;
|
|
4078
|
+
for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) {
|
|
4079
|
+
if (i === 0) return 3;
|
|
4080
|
+
if (!/[a-z0-9]/.test(t[i - 1])) return 2;
|
|
4081
|
+
midWord = true;
|
|
4082
|
+
}
|
|
4083
|
+
if (midWord) return 1;
|
|
4084
|
+
const terms = q.split(/\s+/).filter(Boolean);
|
|
4085
|
+
if (terms.length > 1 && terms.every((term) => t.includes(term))) return 1;
|
|
4086
|
+
return 0;
|
|
4087
|
+
}
|
|
4005
4088
|
function matchDocuments(documents, query) {
|
|
4006
4089
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
4007
4090
|
if (terms.length === 0) return [];
|
|
@@ -4013,15 +4096,25 @@ function matchDocuments(documents, query) {
|
|
|
4013
4096
|
...node.frontmatter.tags || []
|
|
4014
4097
|
].join(" ").toLowerCase();
|
|
4015
4098
|
return terms.every((term) => haystack.includes(term));
|
|
4016
|
-
})
|
|
4099
|
+
}).sort(
|
|
4100
|
+
(a, b) => titleScore(b.frontmatter.title, query) - titleScore(a.frontmatter.title, query)
|
|
4101
|
+
);
|
|
4017
4102
|
}
|
|
4018
|
-
function
|
|
4103
|
+
function bodySnippet(body, query) {
|
|
4104
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
4105
|
+
const terms = (query || "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
4106
|
+
const at = terms.map((t) => flat.toLowerCase().indexOf(t)).filter((i) => i >= 0).sort((a, b) => a - b)[0];
|
|
4107
|
+
if (at === void 0 || at < 80) return flat.slice(0, 200);
|
|
4108
|
+
const start = Math.max(0, at - 60);
|
|
4109
|
+
return `\u2026${flat.slice(start, start + 200)}`;
|
|
4110
|
+
}
|
|
4111
|
+
function toSearchHit(node, query) {
|
|
4019
4112
|
return {
|
|
4020
4113
|
id: node.id,
|
|
4021
4114
|
title: node.frontmatter.title,
|
|
4022
4115
|
type: node.frontmatter.type || "document",
|
|
4023
4116
|
tags: node.frontmatter.tags || [],
|
|
4024
|
-
snippet: (node.body || ""
|
|
4117
|
+
snippet: bodySnippet(node.body || "", query)
|
|
4025
4118
|
};
|
|
4026
4119
|
}
|
|
4027
4120
|
|
|
@@ -4153,7 +4246,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
4153
4246
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
4154
4247
|
const nestId = c.req.param("nestId");
|
|
4155
4248
|
const nodeId = c.req.param("nodeId");
|
|
4156
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
4249
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-OHTTZFND.js");
|
|
4157
4250
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
4158
4251
|
nestId,
|
|
4159
4252
|
nodeId
|
|
@@ -4174,7 +4267,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
4174
4267
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
4175
4268
|
const nestId = c.req.param("nestId");
|
|
4176
4269
|
const nodeId = c.req.param("nodeId");
|
|
4177
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
4270
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-3CCKDO5N.js");
|
|
4178
4271
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
4179
4272
|
return c.json({ reviews: history });
|
|
4180
4273
|
});
|
|
@@ -4298,6 +4391,7 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
4298
4391
|
response.collaborators = people.collaborators;
|
|
4299
4392
|
}
|
|
4300
4393
|
}
|
|
4394
|
+
response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
|
|
4301
4395
|
if (publicReader) {
|
|
4302
4396
|
const approved = await getApprovedVersion(nestId, nodeId);
|
|
4303
4397
|
if (approved != null) {
|
|
@@ -5785,15 +5879,14 @@ queryRoutes.get("/search", async (c) => {
|
|
|
5785
5879
|
}
|
|
5786
5880
|
const nestId = c.req.param("nestId");
|
|
5787
5881
|
const { storage } = await engineCache.get(nestId);
|
|
5788
|
-
const
|
|
5789
|
-
const matches = matchDocuments(documents, q);
|
|
5882
|
+
const matches = matchDocuments(await discoverForSearch(storage), q);
|
|
5790
5883
|
const userId = c.get("userId");
|
|
5791
5884
|
const userEmail = await resolveCallerEmail(userId);
|
|
5792
5885
|
const accessible = await filterAccessible(nestId, userId, userEmail, matches);
|
|
5793
5886
|
return c.json({
|
|
5794
5887
|
query: q,
|
|
5795
5888
|
count: accessible.length,
|
|
5796
|
-
nodes: accessible.map(toSearchHit)
|
|
5889
|
+
nodes: accessible.map((n) => toSearchHit(n, q))
|
|
5797
5890
|
});
|
|
5798
5891
|
});
|
|
5799
5892
|
queryRoutes.get("/overview", async (c) => {
|
|
@@ -5833,10 +5926,6 @@ queryRoutes.get("/context", async (c) => {
|
|
|
5833
5926
|
queryRoutes.get("/export", async (c) => {
|
|
5834
5927
|
const nestId = c.req.param("nestId");
|
|
5835
5928
|
if (c.req.query("format") === "bundle") {
|
|
5836
|
-
const perm = await resolveNestPermission(nestId, c.get("userId"));
|
|
5837
|
-
if (perm !== "owner" && perm !== "admin") {
|
|
5838
|
-
throw new ForbiddenError("Only the nest owner or an admin can export a bundle");
|
|
5839
|
-
}
|
|
5840
5929
|
const draftsParam = c.req.query("includeDrafts");
|
|
5841
5930
|
const includeDrafts = draftsParam === "1" || draftsParam === "true";
|
|
5842
5931
|
const zip = await exportNestBundle(nestId, includeDrafts);
|
|
@@ -7923,6 +8012,13 @@ ${sections}`;
|
|
|
7923
8012
|
(n) => String(n.frontmatter.title ?? "").toLowerCase() === wanted
|
|
7924
8013
|
);
|
|
7925
8014
|
if (twin) {
|
|
8015
|
+
const twinVersions = await getVersions(nestId, twin.id);
|
|
8016
|
+
if (twinVersions.length === 0) {
|
|
8017
|
+
return toolError(
|
|
8018
|
+
`A node titled "${twin.frontmatter.title}" already exists (${twin.id}) but has no version history, which is why it doesn't show up in a read or a draft list. It may be an interrupted create or a document whose import never finished. Write into it with context_update "${twin.id}" to keep whatever is there, or remove it with context_delete "${twin.id}" and create again \u2014 check its content first.`,
|
|
8019
|
+
"DOCUMENT_ALREADY_EXISTS"
|
|
8020
|
+
);
|
|
8021
|
+
}
|
|
7926
8022
|
return toolError(
|
|
7927
8023
|
`A node titled "${twin.frontmatter.title}" already exists (${twin.id}). Titles must be unique \u2014 MCP tools resolve documents by title, so a second one would be unreachable. Use context_update to change it, context_move to refile it, or create this one under a different title.`,
|
|
7928
8024
|
"DOCUMENT_ALREADY_EXISTS"
|
|
@@ -8076,6 +8172,9 @@ ${sections}`;
|
|
|
8076
8172
|
return "reason is required \u2014 say why it should be deleted; that is what the admin judges.";
|
|
8077
8173
|
}
|
|
8078
8174
|
if (args.nest === true) {
|
|
8175
|
+
if (!await canRequestDeletion({ nestId, userId, userEmail, nodeId: "" })) {
|
|
8176
|
+
return "Only an editor, a nest admin, or the nest owner can request the deletion of a nest.";
|
|
8177
|
+
}
|
|
8079
8178
|
const request2 = await requestDeletion({
|
|
8080
8179
|
nestId,
|
|
8081
8180
|
nodeId: nestId,
|
|
@@ -8088,6 +8187,9 @@ ${sections}`;
|
|
|
8088
8187
|
Request id: \`${request2.id}\``;
|
|
8089
8188
|
}
|
|
8090
8189
|
if (typeof args.folder === "string" && args.folder.trim()) {
|
|
8190
|
+
if (!await canRequestDeletion({ nestId, userId, userEmail, nodeId: "" })) {
|
|
8191
|
+
return "Only an editor, a nest admin, or the nest owner can request the deletion of a folder.";
|
|
8192
|
+
}
|
|
8091
8193
|
const folder = args.folder.trim().replace(/^\/+|\/+$/g, "").replace(/^nodes\//, "");
|
|
8092
8194
|
const docs = await storage.discoverDocuments();
|
|
8093
8195
|
const inside = docs.filter((d) => isIdUnderFolder(d.id, folder));
|
|
@@ -8110,6 +8212,14 @@ Request id: \`${request2.id}\``;
|
|
|
8110
8212
|
if (!await canReadNode(nestId, node.id, userId, userEmail)) {
|
|
8111
8213
|
return `Node not found: ${node.id}`;
|
|
8112
8214
|
}
|
|
8215
|
+
if (!await canRequestDeletion({
|
|
8216
|
+
nestId,
|
|
8217
|
+
userId,
|
|
8218
|
+
userEmail,
|
|
8219
|
+
nodeId: node.id
|
|
8220
|
+
})) {
|
|
8221
|
+
return "Only an editor, a nest admin, or the nest owner can request the deletion of a document.";
|
|
8222
|
+
}
|
|
8113
8223
|
const request = await requestDeletion({
|
|
8114
8224
|
nestId,
|
|
8115
8225
|
nodeId: node.id,
|
|
@@ -8434,7 +8544,7 @@ ${list}`,
|
|
|
8434
8544
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
8435
8545
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
8436
8546
|
try {
|
|
8437
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
8547
|
+
const { createGrant: createGrant2 } = await import("./grants-service-4NXPBYWD.js");
|
|
8438
8548
|
const db = (await import("./client-OBS5HOTA.js")).getDb();
|
|
8439
8549
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
8440
8550
|
const e = normalizeEmail2(String(args.email || ""));
|
|
@@ -10024,6 +10134,11 @@ Streamable HTTP, stateless. Two endpoints:
|
|
|
10024
10134
|
(new document in a writable nest; a \`folder\` path creates the folder).
|
|
10025
10135
|
- \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, full toolset including writes and
|
|
10026
10136
|
governance (create/update, submit/approve/reject, stewards, sharing).
|
|
10137
|
+
Sharing a slice is one call: \`context_grant\` with
|
|
10138
|
+
\`{ "email", "target_type": "document" | "folder", "target", "role": "read" | "write" }\`
|
|
10139
|
+
\u2014 \`target\` is a node id (\`nodes/win-loss-log\`) or a folder prefix
|
|
10140
|
+
(\`nodes/gtm/deals\`). The recipient reaches exactly that document, or
|
|
10141
|
+
everything under that folder, and nothing else in the nest. Owner/admin only.
|
|
10027
10142
|
|
|
10028
10143
|
Claude Code / Cursor / VS Code connect natively via HTTP with the
|
|
10029
10144
|
\`Authorization: Bearer\` header. Claude Desktop needs the mcp-remote bridge:
|
|
@@ -10032,20 +10147,9 @@ Claude Code / Cursor / VS Code connect natively via HTTP with the
|
|
|
10032
10147
|
npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
|
|
10033
10148
|
\`\`\`
|
|
10034
10149
|
|
|
10035
|
-
## ctx CLI (npm: @promptowl/contextnest-cli)
|
|
10036
|
-
|
|
10037
|
-
\`npm i -g @promptowl/contextnest-cli\`
|
|
10038
|
-
|
|
10039
|
-
- \`ctx push --server ${B} --nest <nest-id> --key <api-key>\` \u2014 upload a local
|
|
10040
|
-
vault into a nest (add \`--include-drafts\` for unpublished nodes).
|
|
10041
|
-
- All other ctx commands (\`query\`, \`add\`, \`search\`, \`checkpoint\`) operate on
|
|
10042
|
-
a LOCAL vault directory only. For remote reads use the API or MCP above \u2014
|
|
10043
|
-
there is currently no remote \`ctx query\`.
|
|
10044
|
-
|
|
10045
10150
|
## npm packages
|
|
10046
10151
|
|
|
10047
10152
|
- \`@promptowl/contextnest-community\` \u2014 this server
|
|
10048
|
-
- \`@promptowl/contextnest-cli\` \u2014 the ctx CLI
|
|
10049
10153
|
- \`@promptowl/contextnest-engine\` \u2014 vault storage + selector/graph engine
|
|
10050
10154
|
|
|
10051
10155
|
## Conventions
|
|
@@ -10624,6 +10728,25 @@ function denyUnlessNestManager(c, what) {
|
|
|
10624
10728
|
403
|
|
10625
10729
|
);
|
|
10626
10730
|
}
|
|
10731
|
+
async function mayRequestDeletion(c, nestId, nodeId) {
|
|
10732
|
+
return canRequestDeletion({
|
|
10733
|
+
nestId,
|
|
10734
|
+
userId: c.get("userId"),
|
|
10735
|
+
userEmail: await getUserEmail4(c),
|
|
10736
|
+
nodeId,
|
|
10737
|
+
// The middleware promotes open-mode callers; resolving fresh would miss it.
|
|
10738
|
+
permission: c.get("nestPermission")
|
|
10739
|
+
});
|
|
10740
|
+
}
|
|
10741
|
+
function denyUnlessEditor(allowed, c, what) {
|
|
10742
|
+
if (allowed) return null;
|
|
10743
|
+
return c.json(
|
|
10744
|
+
{
|
|
10745
|
+
error: `Only an editor, a nest admin, or the nest owner can request the deletion of ${what}.`
|
|
10746
|
+
},
|
|
10747
|
+
403
|
|
10748
|
+
);
|
|
10749
|
+
}
|
|
10627
10750
|
governanceRoutes.post("/delete-folder", async (c) => {
|
|
10628
10751
|
const nestId = c.req.param("nestId");
|
|
10629
10752
|
const denied = denyUnlessNestManager(c, "a folder");
|
|
@@ -10641,6 +10764,12 @@ governanceRoutes.post("/delete-folder", async (c) => {
|
|
|
10641
10764
|
});
|
|
10642
10765
|
governanceRoutes.post("/request-deletion", async (c) => {
|
|
10643
10766
|
const nestId = c.req.param("nestId");
|
|
10767
|
+
const denied = denyUnlessEditor(
|
|
10768
|
+
await mayRequestDeletion(c, nestId, ""),
|
|
10769
|
+
c,
|
|
10770
|
+
"a folder or a nest"
|
|
10771
|
+
);
|
|
10772
|
+
if (denied) return denied;
|
|
10644
10773
|
const body = await c.req.json().catch(() => ({}));
|
|
10645
10774
|
const reason = body.reason?.trim();
|
|
10646
10775
|
if (!reason) {
|
|
@@ -10731,6 +10860,16 @@ governanceRoutes.post("/deletion-requests/:id/decline", async (c) => {
|
|
|
10731
10860
|
if (!request) return c.json({ error: "Deletion request not found" }, 404);
|
|
10732
10861
|
return c.json({ request });
|
|
10733
10862
|
});
|
|
10863
|
+
governanceRoutes.post("/deletion-requests/:id/withdraw", async (c) => {
|
|
10864
|
+
const nestId = c.req.param("nestId");
|
|
10865
|
+
const request = await withdrawDeletionRequest({
|
|
10866
|
+
nestId,
|
|
10867
|
+
id: c.req.param("id"),
|
|
10868
|
+
withdrawnBy: await getUserEmail4(c)
|
|
10869
|
+
});
|
|
10870
|
+
if (!request) return c.json({ error: "Deletion request not found or not yours" }, 404);
|
|
10871
|
+
return c.json({ withdrawn: true, request });
|
|
10872
|
+
});
|
|
10734
10873
|
governanceRoutes.get("/feed", async (c) => {
|
|
10735
10874
|
const nestId = c.req.param("nestId");
|
|
10736
10875
|
const filterRaw = c.req.query("filter");
|
|
@@ -10988,6 +11127,12 @@ governanceNodeRoutes.post("/:nodeId{.+}/request-deletion", async (c) => {
|
|
|
10988
11127
|
if (!await canReadNode(nestId, nodeId, c.get("userId"), userEmail)) {
|
|
10989
11128
|
return c.json({ error: `Node not found: ${nodeId}` }, 404);
|
|
10990
11129
|
}
|
|
11130
|
+
const denied = denyUnlessEditor(
|
|
11131
|
+
await mayRequestDeletion(c, nestId, nodeId),
|
|
11132
|
+
c,
|
|
11133
|
+
"a document"
|
|
11134
|
+
);
|
|
11135
|
+
if (denied) return denied;
|
|
10991
11136
|
const { storage } = await engineCache.get(nestId);
|
|
10992
11137
|
try {
|
|
10993
11138
|
await storage.readDocument(nodeId);
|
|
@@ -12102,14 +12247,14 @@ function createApp() {
|
|
|
12102
12247
|
const userId = c.get("userId");
|
|
12103
12248
|
const userEmail = await resolveCallerEmail(userId);
|
|
12104
12249
|
const perNest = await Promise.all(
|
|
12105
|
-
(await listVisibleNests(userId)).map(async (nest) => {
|
|
12250
|
+
(await listVisibleNests(userId, { readable: true })).map(async (nest) => {
|
|
12106
12251
|
try {
|
|
12107
12252
|
const { storage } = await engineCache.get(nest.id);
|
|
12108
|
-
const matches = matchDocuments(await storage
|
|
12253
|
+
const matches = matchDocuments(await discoverForSearch(storage), q);
|
|
12109
12254
|
if (matches.length === 0) return [];
|
|
12110
12255
|
const accessible = await filterAccessible(nest.id, userId, userEmail, matches);
|
|
12111
12256
|
return accessible.map((n) => ({
|
|
12112
|
-
...toSearchHit(n),
|
|
12257
|
+
...toSearchHit(n, q),
|
|
12113
12258
|
nest_id: nest.id,
|
|
12114
12259
|
nest_name: nest.name ?? nest.id
|
|
12115
12260
|
}));
|
|
@@ -12119,7 +12264,7 @@ function createApp() {
|
|
|
12119
12264
|
}
|
|
12120
12265
|
})
|
|
12121
12266
|
);
|
|
12122
|
-
const hits = perNest.flat();
|
|
12267
|
+
const hits = perNest.flat().sort((a, b) => titleScore(b.title, q) - titleScore(a.title, q));
|
|
12123
12268
|
return c.json({
|
|
12124
12269
|
query: q,
|
|
12125
12270
|
count: hits.length,
|
|
@@ -12195,7 +12340,8 @@ function createApp() {
|
|
|
12195
12340
|
c.set("nestPermission", "owner");
|
|
12196
12341
|
return next();
|
|
12197
12342
|
}
|
|
12198
|
-
const
|
|
12343
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
12344
|
+
const permission = access.permission;
|
|
12199
12345
|
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
|
|
12200
12346
|
let grantRole = null;
|
|
12201
12347
|
let grantNestVisible = false;
|
|
@@ -12208,11 +12354,13 @@ function createApp() {
|
|
|
12208
12354
|
}
|
|
12209
12355
|
grantRole = await resolveNodeGrant(nestId, userId, targetTail);
|
|
12210
12356
|
} else if (parts.length === 1 && c.req.method === "GET") {
|
|
12211
|
-
grantNestVisible =
|
|
12357
|
+
grantNestVisible = access.hasGrant;
|
|
12212
12358
|
} else if (parts[1] === "nodes" && parts.length === 2 && c.req.method === "GET") {
|
|
12213
|
-
grantNestVisible =
|
|
12359
|
+
grantNestVisible = access.hasGrant;
|
|
12214
12360
|
} else if (isReadQuery) {
|
|
12215
|
-
grantNestVisible =
|
|
12361
|
+
grantNestVisible = access.hasGrant;
|
|
12362
|
+
} else if (parts[1] === "definitions" && parts.length === 2 && c.req.method === "GET") {
|
|
12363
|
+
grantNestVisible = access.hasGrant;
|
|
12216
12364
|
}
|
|
12217
12365
|
if (!grantRole && !grantNestVisible) {
|
|
12218
12366
|
return deny(404, "Nest not found", {
|
|
@@ -12225,7 +12373,7 @@ function createApp() {
|
|
|
12225
12373
|
const isStewardActionPath = path.includes("/approve") || path.includes("/reject") || path.includes("/submit-review") || path.includes("/cancel-review");
|
|
12226
12374
|
const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
|
|
12227
12375
|
const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
|
|
12228
|
-
const isDeletionRequestAction = /\/request-deletion$/.test(path);
|
|
12376
|
+
const isDeletionRequestAction = /\/request-deletion$/.test(path) || /\/deletion-requests\/[^/]+\/withdraw$/.test(path);
|
|
12229
12377
|
const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
|
|
12230
12378
|
if (isStewardRoster && permission !== "owner" && permission !== "admin") {
|
|
12231
12379
|
return c.json(
|
|
@@ -14,14 +14,15 @@ import {
|
|
|
14
14
|
reject,
|
|
15
15
|
repointPendingReview,
|
|
16
16
|
requestDeletion,
|
|
17
|
-
submitForReview
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import "./chunk-
|
|
17
|
+
submitForReview,
|
|
18
|
+
withdrawDeletionRequest
|
|
19
|
+
} from "./chunk-4YGJSUJC.js";
|
|
20
|
+
import "./chunk-HWDPCPYT.js";
|
|
21
|
+
import "./chunk-UXJSO54R.js";
|
|
21
22
|
import {
|
|
22
23
|
canUserApprove
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-YF4OFT2V.js";
|
|
25
|
+
import "./chunk-SNFGU5Q7.js";
|
|
25
26
|
import "./chunk-GUNJTORH.js";
|
|
26
27
|
import "./chunk-JMJLNEXE.js";
|
|
27
28
|
import "./chunk-SLTQACJW.js";
|
|
@@ -43,5 +44,6 @@ export {
|
|
|
43
44
|
reject,
|
|
44
45
|
repointPendingReview,
|
|
45
46
|
requestDeletion,
|
|
46
|
-
submitForReview
|
|
47
|
+
submitForReview,
|
|
48
|
+
withdrawDeletionRequest
|
|
47
49
|
};
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
syncFromConfig,
|
|
22
22
|
updateSteward,
|
|
23
23
|
updateStewardRole
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-YF4OFT2V.js";
|
|
25
|
+
import "./chunk-SNFGU5Q7.js";
|
|
26
26
|
import "./chunk-GUNJTORH.js";
|
|
27
27
|
import "./chunk-JMJLNEXE.js";
|
|
28
28
|
import "./chunk-SLTQACJW.js";
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
setApprovedVersion,
|
|
16
16
|
systemAuthor,
|
|
17
17
|
upsertVersion
|
|
18
|
-
} from "./chunk-
|
|
19
|
-
import "./chunk-
|
|
18
|
+
} from "./chunk-UXJSO54R.js";
|
|
19
|
+
import "./chunk-SNFGU5Q7.js";
|
|
20
20
|
import "./chunk-GUNJTORH.js";
|
|
21
21
|
import "./chunk-JMJLNEXE.js";
|
|
22
22
|
import "./chunk-SLTQACJW.js";
|