@promptowl/contextnest-community 1.24.0 → 1.25.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/API.md +109 -6
- package/CONFIGURATION.md +7 -2
- package/README.md +1 -1
- package/STEWARDSHIP.md +24 -0
- package/dist/{chunk-2TPQTN4Y.js → chunk-2KHM5C7V.js} +37 -3
- package/dist/{chunk-XUIWAWDO.js → chunk-32SZCH36.js} +4 -4
- package/dist/{chunk-KIAAEHWL.js → chunk-7FROMOZM.js} +23 -5
- package/dist/{chunk-5EZOPA47.js → chunk-DTLMBF4W.js} +10 -8
- package/dist/{chunk-LA3VTQ22.js → chunk-JDAOA5KP.js} +43 -1
- package/dist/{chunk-I3CSD6CK.js → chunk-KPGNZLZL.js} +2 -2
- package/dist/{chunk-YVMSM7LS.js → chunk-LHWOJPFE.js} +7 -0
- package/dist/{chunk-ZTT4U4NE.js → chunk-TI7HMP64.js} +99 -12
- package/dist/{client-KEY4PYJH.js → client-D44ZHDTJ.js} +1 -1
- package/dist/{engine-S3QBQ7LH.js → engine-TER6FDBP.js} +3 -3
- package/dist/{external-edit-service-6FNFPOJJ.js → external-edit-service-CAGENHDJ.js} +4 -4
- package/dist/{grants-service-UT3EQ3R7.js → grants-service-2OTFS7EH.js} +3 -3
- package/dist/index.js +863 -355
- package/dist/{migrations.postgres-AIQ7WSU7.js → migrations.postgres-23VJJJJX.js} +12 -2
- package/dist/{review-service-HHGOTO6P.js → review-service-4G2XUFS6.js} +7 -7
- package/dist/{stewardship-service-4DHNRULG.js → stewardship-service-S22V2JKG.js} +6 -4
- package/dist/{version-service-KIOQME6U.js → version-service-X4FAG2OZ.js} +4 -4
- package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +1 -0
- package/dist/web3/assets/{AgentDocsPage-CIiaBqMy.js → AgentDocsPage-Dkhgb1hk.js} +1 -1
- package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +1 -0
- package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +36 -0
- package/dist/web3/assets/{DocumentsTab-Vba-AzBv.js → DocumentsTab-BUxBCN8o.js} +2 -2
- package/dist/web3/assets/{ExternalEditsTab-BSzAQIGM.js → ExternalEditsTab-kIBimZyE.js} +1 -1
- package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +1 -0
- package/dist/web3/assets/MarkdownEditor-Dfk5uIE5.js +648 -0
- package/dist/web3/assets/{NestPageHeader-CoLUV1Oz.js → NestPageHeader-yOK-OIYZ.js} +1 -1
- package/dist/web3/assets/NestView-DwIagxwJ.js +68 -0
- package/dist/web3/assets/{OverviewTab-Dxi-Hu-N.js → OverviewTab-DFOFMVyE.js} +1 -1
- package/dist/web3/assets/{PersonCombobox-CPOlNs6G.js → PersonCombobox-D350WlrJ.js} +1 -1
- package/dist/web3/assets/{ReasonDialog-DgLxfRWv.js → ReasonDialog-CN3ECAnQ.js} +1 -1
- package/dist/web3/assets/{ReviewActions-OfOdqzCn.js → ReviewActions-BXDTDPp1.js} +1 -1
- package/dist/web3/assets/{ReviewTab-CyX8yxd3.js → ReviewTab-CZewAYiz.js} +1 -1
- package/dist/web3/assets/StewardsTab-AagNWcWS.js +1 -0
- package/dist/web3/assets/{SubmitForReviewModal-Ci2DdWs0.js → SubmitForReviewModal-BOZJxwRN.js} +1 -1
- package/dist/web3/assets/{alert-dialog-DTozKlmV.js → alert-dialog-4SiLKlz8.js} +1 -1
- package/dist/web3/assets/{arrow-left-BCKt4CwQ.js → arrow-left-BAgIocLn.js} +1 -1
- package/dist/web3/assets/backlinks-Cb8Uf8mw.js +24 -0
- package/dist/web3/assets/{card-XG2dP5Xf.js → card-UDyMRcps.js} +1 -1
- package/dist/web3/assets/{chevron-left-BPwUT9DS.js → chevron-left-XR4ReJ9Z.js} +1 -1
- package/dist/web3/assets/{circle-check-DI8IBGeo.js → circle-check-CSkZUEFK.js} +1 -1
- package/dist/web3/assets/{circle-x-VKVhC88W.js → circle-x-in2o3aHU.js} +1 -1
- package/dist/web3/assets/client-attribution-BraarYaP.js +1 -0
- package/dist/web3/assets/{code-xml-CMabYZvH.js → code-xml-pPQgdctI.js} +1 -1
- package/dist/web3/assets/{corner-down-right-BGxwK6wH.js → corner-down-right-CuxZWscX.js} +1 -1
- package/dist/web3/assets/{count-skeleton-DMbdpscX.js → count-skeleton-DBWCDhRy.js} +1 -1
- package/dist/web3/assets/{earth-BEp1EKTo.js → earth-CXOcThqn.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-r8qZGIjP.js → file-exclamation-point-CSphs1VD.js} +1 -1
- package/dist/web3/assets/{folder-input-Cmtx1Jhk.js → folder-input-BAwK9tFL.js} +1 -1
- package/dist/web3/assets/{index-C1wTSjfP.js → index-A0_ymcqL.js} +1 -1
- package/dist/web3/assets/index-B5hLclEq.js +389 -0
- package/dist/web3/assets/index-DrUAtoQM.css +1 -0
- package/dist/web3/assets/page-BHD4beun.js +1 -0
- package/dist/web3/assets/{page-CPjWQNKx.js → page-BSYUXYmO.js} +1 -1
- package/dist/web3/assets/page-BYc2DIux.js +45 -0
- package/dist/web3/assets/{page-Bicf02Cz.js → page-BfibLg8e.js} +1 -1
- package/dist/web3/assets/page-CGe8MtUu.js +9 -0
- package/dist/web3/assets/{page-CQt3ZGbf.js → page-CIgPJmG6.js} +1 -1
- package/dist/web3/assets/{page-CHb0F5hV.js → page-CUjuSs5Q.js} +1 -1
- package/dist/web3/assets/{page-BYUFhYWZ.js → page-Ck31nx9b.js} +1 -1
- package/dist/web3/assets/{page-BExMvttJ.js → page-Cpdj_11Y.js} +1 -1
- package/dist/web3/assets/{page-CtaW65El.js → page-D0sN3GO6.js} +1 -1
- package/dist/web3/assets/page-DEpH91X2.js +1 -0
- package/dist/web3/assets/{page-CC0NJi8v.js → page-l8-B7CGt.js} +1 -1
- package/dist/web3/assets/{page-DGOL9l8i.js → page-mnSHmk6A.js} +1 -1
- package/dist/web3/assets/{page-title-ClvvBsIo.js → page-title-BkqlUA0j.js} +1 -1
- package/dist/web3/assets/{page-ByyVLDVo.js → page-xzYqkBUP.js} +5 -5
- package/dist/web3/assets/{play-C5YNKpAH.js → play-B8xE9j5f.js} +1 -1
- package/dist/web3/assets/{refresh-cw-BhHzSH9m.js → refresh-cw-CsCtmWwX.js} +1 -1
- package/dist/web3/assets/{scroll-area-hz-xtayP.js → scroll-area-CbcH6yrZ.js} +1 -1
- package/dist/web3/assets/{select-DBFTrUmI.js → select-DrL0mpRN.js} +2 -2
- package/dist/web3/assets/{send-0CHvmluT.js → send-CvAgOUPG.js} +1 -1
- package/dist/web3/assets/{settings-CeeF4LEb.js → settings-EY4A_DYv.js} +1 -1
- package/dist/web3/assets/{share-2-CMnOlOt-.js → share-2-vGmZUl90.js} +1 -1
- package/dist/web3/assets/{tag-DQ_6J5Gv.js → tag-Br_y1f00.js} +1 -1
- package/dist/web3/assets/{trash-2-HBi-Pslz.js → trash-2-B4hRXo-p.js} +1 -1
- package/dist/web3/assets/{triangle-alert-CDy8-7sv.js → triangle-alert-CsYGyfGZ.js} +1 -1
- package/dist/web3/assets/{user-plus-CvBNcpn4.js → user-plus-4CTYEkd3.js} +1 -1
- package/dist/web3/assets/{x-By7piikG.js → x-qxt1WrUi.js} +1 -1
- package/dist/web3/assets/zap-BcsKR0ef.js +16 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -2
- package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +0 -1
- package/dist/web3/assets/CollaboratorManager-By91wrIr.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +0 -1
- package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +0 -36
- package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +0 -1
- package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +0 -643
- package/dist/web3/assets/NestView-CTZ55tXD.js +0 -63
- package/dist/web3/assets/StewardsTab-DBEEfAQI.js +0 -1
- package/dist/web3/assets/backlinks-CYd4xENL.js +0 -24
- package/dist/web3/assets/index-BM-h3DwI.css +0 -1
- package/dist/web3/assets/index-EaX2yql0.js +0 -389
- package/dist/web3/assets/page-B3yvQvHy.js +0 -1
- package/dist/web3/assets/page-BUOADv2X.js +0 -1
- package/dist/web3/assets/page-BsPMDm5d.js +0 -45
- package/dist/web3/assets/page-Cj1NetQ-.js +0 -1
- package/dist/web3/assets/zap-C2T8riBp.js +0 -11
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
submitForReview,
|
|
39
39
|
withNodeWriteLock,
|
|
40
40
|
withdrawDeletionRequest
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-DTLMBF4W.js";
|
|
42
42
|
import {
|
|
43
43
|
canCreateInNest,
|
|
44
44
|
canManageStewards,
|
|
@@ -56,9 +56,10 @@ import {
|
|
|
56
56
|
resolveStewardsForNode,
|
|
57
57
|
resolveStewardsWithFallback,
|
|
58
58
|
resolveUserRoles,
|
|
59
|
+
stewardDocumentCreator,
|
|
59
60
|
syncFromConfig,
|
|
60
61
|
updateSteward
|
|
61
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-2KHM5C7V.js";
|
|
62
63
|
import {
|
|
63
64
|
createGrant,
|
|
64
65
|
deleteGrant,
|
|
@@ -67,7 +68,7 @@ import {
|
|
|
67
68
|
listGrants,
|
|
68
69
|
listUserGrants,
|
|
69
70
|
resolveNodeGrant
|
|
70
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-KPGNZLZL.js";
|
|
71
72
|
import {
|
|
72
73
|
generateApiKey,
|
|
73
74
|
getKeyPrefix,
|
|
@@ -78,10 +79,14 @@ import {
|
|
|
78
79
|
verifyPassword
|
|
79
80
|
} from "./chunk-F66VCBLA.js";
|
|
80
81
|
import {
|
|
82
|
+
attributeRequest,
|
|
81
83
|
checkConflict,
|
|
84
|
+
clientToolField,
|
|
85
|
+
connectionClient,
|
|
82
86
|
createVersion,
|
|
83
87
|
deleteSupersededDrafts,
|
|
84
88
|
deleteUnsealedVersions,
|
|
89
|
+
deserializeClient,
|
|
85
90
|
getApprovedVersion,
|
|
86
91
|
getApprovedVersions,
|
|
87
92
|
getCurrentVersion,
|
|
@@ -91,9 +96,12 @@ import {
|
|
|
91
96
|
getTrackedNodeIds,
|
|
92
97
|
getVersions,
|
|
93
98
|
listMyDrafts,
|
|
99
|
+
mergeClient,
|
|
100
|
+
parseClientMetadata,
|
|
101
|
+
serializeClient,
|
|
94
102
|
setApprovedVersion,
|
|
95
103
|
upsertVersion
|
|
96
|
-
} from "./chunk-
|
|
104
|
+
} from "./chunk-TI7HMP64.js";
|
|
97
105
|
import {
|
|
98
106
|
approveExternalEdit,
|
|
99
107
|
getExternalEditDetail,
|
|
@@ -105,7 +113,7 @@ import {
|
|
|
105
113
|
scanDocumentForDrift,
|
|
106
114
|
scanNestForDrift,
|
|
107
115
|
startDriftScanner
|
|
108
|
-
} from "./chunk-
|
|
116
|
+
} from "./chunk-32SZCH36.js";
|
|
109
117
|
import {
|
|
110
118
|
addMember,
|
|
111
119
|
addWatcher,
|
|
@@ -162,6 +170,7 @@ import {
|
|
|
162
170
|
loadServerSettings,
|
|
163
171
|
markNotificationsRead,
|
|
164
172
|
nestAllowsSelfApprove,
|
|
173
|
+
nestCreatorIsReviewer,
|
|
165
174
|
nestName,
|
|
166
175
|
nestPrimeTags,
|
|
167
176
|
nestReviewsPrimeOnly,
|
|
@@ -195,6 +204,7 @@ import {
|
|
|
195
204
|
restoreNest,
|
|
196
205
|
sendEmailToRecipient,
|
|
197
206
|
setAllowSelfApprove,
|
|
207
|
+
setCreatorIsReviewer,
|
|
198
208
|
setPrimeOnlyReview,
|
|
199
209
|
setPrimeTags,
|
|
200
210
|
setReaderMode,
|
|
@@ -211,25 +221,26 @@ import {
|
|
|
211
221
|
updateMemberRole,
|
|
212
222
|
validateLicense,
|
|
213
223
|
verifySmtp
|
|
214
|
-
} from "./chunk-
|
|
224
|
+
} from "./chunk-7FROMOZM.js";
|
|
215
225
|
import {
|
|
216
226
|
AppError,
|
|
217
227
|
ConflictError,
|
|
218
228
|
ForbiddenError,
|
|
219
229
|
LockedError,
|
|
220
230
|
NotFoundError,
|
|
231
|
+
PayloadTooLargeError,
|
|
221
232
|
UnauthorizedError,
|
|
222
233
|
ValidationError,
|
|
223
234
|
VaultBusyError,
|
|
224
235
|
isVaultLockTimeout
|
|
225
|
-
} from "./chunk-
|
|
236
|
+
} from "./chunk-LHWOJPFE.js";
|
|
226
237
|
import {
|
|
227
238
|
config,
|
|
228
239
|
getDb,
|
|
229
240
|
initDb,
|
|
230
241
|
isEmailListish,
|
|
231
242
|
isEmailish
|
|
232
|
-
} from "./chunk-
|
|
243
|
+
} from "./chunk-JDAOA5KP.js";
|
|
233
244
|
import {
|
|
234
245
|
ANON_EMAIL,
|
|
235
246
|
ANON_USER_ID,
|
|
@@ -243,7 +254,7 @@ import {
|
|
|
243
254
|
import { serve } from "@hono/node-server";
|
|
244
255
|
|
|
245
256
|
// src/app.ts
|
|
246
|
-
import { Hono as
|
|
257
|
+
import { Hono as Hono26 } from "hono";
|
|
247
258
|
import { LinearRouter } from "hono/router/linear-router";
|
|
248
259
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
249
260
|
import { cors } from "hono/cors";
|
|
@@ -430,7 +441,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
430
441
|
if (key) {
|
|
431
442
|
const keyHash = hashApiKey(key);
|
|
432
443
|
const record = await db.get(
|
|
433
|
-
"SELECT user_id, nest_id FROM api_keys WHERE key_hash = ?",
|
|
444
|
+
"SELECT user_id, nest_id, label FROM api_keys WHERE key_hash = ?",
|
|
434
445
|
[keyHash]
|
|
435
446
|
);
|
|
436
447
|
if (record) {
|
|
@@ -440,10 +451,26 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
440
451
|
);
|
|
441
452
|
c.set("userId", record.user_id);
|
|
442
453
|
c.set("nestScope", record.nest_id);
|
|
454
|
+
if (record.label) c.set("apiKeyLabel", record.label);
|
|
443
455
|
return next();
|
|
444
456
|
}
|
|
445
457
|
}
|
|
446
|
-
|
|
458
|
+
if (sessionId) {
|
|
459
|
+
return c.json(
|
|
460
|
+
{ error: "Your session has expired \u2014 sign in again.", code: "session_expired" },
|
|
461
|
+
401
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (key) {
|
|
465
|
+
return c.json(
|
|
466
|
+
{ error: "This API key is invalid or has been revoked \u2014 generate a new one.", code: "invalid_key" },
|
|
467
|
+
401
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return c.json(
|
|
471
|
+
{ error: "Authentication required \u2014 sign in or provide an API key.", code: "missing_credentials" },
|
|
472
|
+
401
|
|
473
|
+
);
|
|
447
474
|
});
|
|
448
475
|
|
|
449
476
|
// src/shared/rate-limit.ts
|
|
@@ -1986,7 +2013,9 @@ import {
|
|
|
1986
2013
|
DocumentNotFoundError,
|
|
1987
2014
|
NODE_TYPES,
|
|
1988
2015
|
TAG_PATTERN,
|
|
1989
|
-
generateIndexMd
|
|
2016
|
+
generateIndexMd,
|
|
2017
|
+
readPdfBinary,
|
|
2018
|
+
pdfSidecarPath
|
|
1990
2019
|
} from "@promptowl/contextnest-engine";
|
|
1991
2020
|
import { rename, mkdir, rm, stat } from "fs/promises";
|
|
1992
2021
|
import { dirname, join } from "path";
|
|
@@ -2566,8 +2595,20 @@ var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2566
2595
|
...NODE_TYPES,
|
|
2567
2596
|
...RUNNABLE_NODE_TYPES,
|
|
2568
2597
|
"artifact",
|
|
2569
|
-
"table"
|
|
2598
|
+
"table",
|
|
2599
|
+
"pdf"
|
|
2570
2600
|
]);
|
|
2601
|
+
var PDF_NODE_TYPE = "pdf";
|
|
2602
|
+
var PDF_UPLOAD_HINT = "upload the file to POST /nests/:nestId/nodes/pdf (the Upload PDF action)";
|
|
2603
|
+
function pdfEngineError(err) {
|
|
2604
|
+
const code = err?.code;
|
|
2605
|
+
const message = err?.message || "The PDF could not be imported.";
|
|
2606
|
+
if (code === "VALIDATION_FAILED" || code === "INVALID_DOCUMENT_ID") {
|
|
2607
|
+
return new ValidationError(message);
|
|
2608
|
+
}
|
|
2609
|
+
if (code === "DOCUMENT_ALREADY_EXISTS") return new ConflictError(message);
|
|
2610
|
+
return err;
|
|
2611
|
+
}
|
|
2571
2612
|
function assertAcceptedType(type) {
|
|
2572
2613
|
if (type === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
|
|
2573
2614
|
throw new ValidationError("Artifact nodes are disabled on this server");
|
|
@@ -2581,6 +2622,13 @@ function assertAcceptedType(type) {
|
|
|
2581
2622
|
);
|
|
2582
2623
|
}
|
|
2583
2624
|
}
|
|
2625
|
+
function assertUpdatableType(storedType) {
|
|
2626
|
+
const type = storedType || "document";
|
|
2627
|
+
if (ACCEPTED_NODE_TYPES.has(type)) return;
|
|
2628
|
+
throw new ValidationError(
|
|
2629
|
+
`node has legacy type "${type}" which this server no longer accepts; include "type": "<accepted type>" in the same PATCH to migrate it (allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")})`
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2584
2632
|
function toNodeResponse(node) {
|
|
2585
2633
|
const fm = node.frontmatter;
|
|
2586
2634
|
const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
|
|
@@ -2605,7 +2653,8 @@ function toNodeResponse(node) {
|
|
|
2605
2653
|
// Same YAML-coercion rule as title above: `schedule: 123` parses as a
|
|
2606
2654
|
// number — normalize to string at the API boundary.
|
|
2607
2655
|
schedule: fm.metadata?.schedule != null ? String(fm.metadata.schedule) : void 0,
|
|
2608
|
-
prime: documentPrimeFlag(fm.metadata)
|
|
2656
|
+
prime: documentPrimeFlag(fm.metadata),
|
|
2657
|
+
...fm.type === "pdf" && fm.pdf ? { pdf: fm.pdf } : {}
|
|
2609
2658
|
};
|
|
2610
2659
|
}
|
|
2611
2660
|
async function resolveAuthorship(nestId, nodes) {
|
|
@@ -2998,9 +3047,11 @@ async function assertMayFlagPrime(nestId, userEmail) {
|
|
|
2998
3047
|
);
|
|
2999
3048
|
}
|
|
3000
3049
|
}
|
|
3001
|
-
async function autoPublishReason(nestId, userEmail, doc) {
|
|
3002
|
-
|
|
3050
|
+
async function autoPublishReason(nestId, userEmail, doc, opts = {}) {
|
|
3051
|
+
const governed = opts.governed ?? await isStewardshipEnabled(nestId);
|
|
3052
|
+
if (!governed) return "no stewards configured";
|
|
3003
3053
|
if (await isPrimeDocument(nestId, doc.tags, doc.metadata)) return null;
|
|
3054
|
+
if (opts.creatorReviews) return "creator is the document's reviewer";
|
|
3004
3055
|
if (await nestReviewsPrimeOnly(nestId)) {
|
|
3005
3056
|
return "prime-only review, document is not prime";
|
|
3006
3057
|
}
|
|
@@ -3030,8 +3081,14 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3030
3081
|
id = `${prefix}${slug || await generatedSlug(storage, prefix)}`;
|
|
3031
3082
|
}
|
|
3032
3083
|
const tags = requireTags(input.tags);
|
|
3033
|
-
const requestedType = input.type || "document";
|
|
3084
|
+
const requestedType = input.pdf ? input.type || PDF_NODE_TYPE : input.type || "document";
|
|
3034
3085
|
assertAcceptedType(requestedType);
|
|
3086
|
+
if (requestedType === PDF_NODE_TYPE && !input.pdf) {
|
|
3087
|
+
throw new ValidationError(`PDF documents can't be created from text \u2014 ${PDF_UPLOAD_HINT}.`);
|
|
3088
|
+
}
|
|
3089
|
+
if (input.pdf && requestedType !== PDF_NODE_TYPE) {
|
|
3090
|
+
throw new ValidationError(`a PDF upload creates a "pdf" node, not "${requestedType}"`);
|
|
3091
|
+
}
|
|
3035
3092
|
if (input.schedule !== void 0 && !isRunnableType(input.type)) {
|
|
3036
3093
|
throw new ValidationError(
|
|
3037
3094
|
`schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
|
|
@@ -3076,42 +3133,85 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3076
3133
|
...input.schedule ? { schedule: input.schedule } : {},
|
|
3077
3134
|
...input.prime === void 0 ? {} : { prime: input.prime }
|
|
3078
3135
|
};
|
|
3079
|
-
const
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
let published = publishReason !== null;
|
|
3136
|
+
const governed = await isStewardshipEnabled(nestId);
|
|
3137
|
+
const creatorReviews = opts.creatorReviews !== false && governed && await nestCreatorIsReviewer(nestId);
|
|
3138
|
+
const creatorSteward = creatorReviews ? await stewardDocumentCreator(nestId, id, userEmail) : null;
|
|
3139
|
+
let published = false;
|
|
3140
|
+
let publishNote;
|
|
3085
3141
|
try {
|
|
3086
|
-
await
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
content: input.content,
|
|
3092
|
-
type: requestedType,
|
|
3093
|
-
// A skill node is invalid without a skill block, and this surface has
|
|
3094
|
-
// no field for one — default the trigger from the title, same as the
|
|
3095
|
-
// CLI and mcp-server do.
|
|
3096
|
-
...requestedType === "skill" ? { trigger: `when asked to ${input.title.toLowerCase()}` } : {},
|
|
3097
|
-
...tags.length ? { tags } : {},
|
|
3098
|
-
...input.status ? { status: input.status } : {},
|
|
3099
|
-
metadata,
|
|
3100
|
-
publish: published,
|
|
3101
|
-
...publishNote ? { note: publishNote } : {}
|
|
3102
|
-
},
|
|
3103
|
-
await opContext(nestId, userEmail)
|
|
3142
|
+
const publishReason = await autoPublishReason(
|
|
3143
|
+
nestId,
|
|
3144
|
+
userEmail,
|
|
3145
|
+
{ tags, metadata },
|
|
3146
|
+
{ creatorReviews: creatorSteward?.canApprove === true, governed }
|
|
3104
3147
|
);
|
|
3105
|
-
|
|
3106
|
-
|
|
3148
|
+
publishNote = publishReason === null ? void 0 : `Auto-published on create (${publishReason})`;
|
|
3149
|
+
published = publishReason !== null;
|
|
3107
3150
|
try {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3151
|
+
if (input.pdf) {
|
|
3152
|
+
try {
|
|
3153
|
+
await engineApi.run(
|
|
3154
|
+
"context_import_pdf",
|
|
3155
|
+
{
|
|
3156
|
+
id,
|
|
3157
|
+
title: input.title,
|
|
3158
|
+
bytes_base64: Buffer.from(input.pdf.bytes).toString("base64"),
|
|
3159
|
+
...input.pdf.filename ? { filename: input.pdf.filename } : {},
|
|
3160
|
+
...tags.length ? { tags } : {},
|
|
3161
|
+
publish: published,
|
|
3162
|
+
...publishNote ? { note: publishNote } : {},
|
|
3163
|
+
...input.client ? { client: input.client } : {}
|
|
3164
|
+
},
|
|
3165
|
+
await opContext(nestId, userEmail)
|
|
3166
|
+
);
|
|
3167
|
+
} catch (err) {
|
|
3168
|
+
throw pdfEngineError(err);
|
|
3169
|
+
}
|
|
3170
|
+
} else {
|
|
3171
|
+
await engineApi.run(
|
|
3172
|
+
"context_create",
|
|
3173
|
+
{
|
|
3174
|
+
id,
|
|
3175
|
+
title: input.title,
|
|
3176
|
+
content: input.content,
|
|
3177
|
+
type: requestedType,
|
|
3178
|
+
// A skill node is invalid without a skill block, and this surface
|
|
3179
|
+
// has no field for one — default the trigger from the title, same
|
|
3180
|
+
// as the CLI and mcp-server do.
|
|
3181
|
+
...requestedType === "skill" ? { trigger: `when asked to ${input.title.toLowerCase()}` } : {},
|
|
3182
|
+
...tags.length ? { tags } : {},
|
|
3183
|
+
...input.status ? { status: input.status } : {},
|
|
3184
|
+
metadata,
|
|
3185
|
+
publish: published,
|
|
3186
|
+
...publishNote ? { note: publishNote } : {}
|
|
3187
|
+
},
|
|
3188
|
+
await opContext(nestId, userEmail)
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
} catch (err) {
|
|
3192
|
+
let onDisk = true;
|
|
3193
|
+
try {
|
|
3194
|
+
await storage.readDocument(id);
|
|
3195
|
+
} catch {
|
|
3196
|
+
onDisk = false;
|
|
3197
|
+
}
|
|
3198
|
+
if (!onDisk) throw err;
|
|
3199
|
+
console.error("publishDocument failed (node create auto-publish)", err);
|
|
3200
|
+
published = false;
|
|
3201
|
+
}
|
|
3202
|
+
} catch (err) {
|
|
3203
|
+
if (creatorSteward?.insertedId) {
|
|
3204
|
+
try {
|
|
3205
|
+
await removeSteward(creatorSteward.insertedId);
|
|
3206
|
+
} catch (cleanupErr) {
|
|
3207
|
+
console.error(
|
|
3208
|
+
"removeSteward failed while compensating a failed create",
|
|
3209
|
+
id,
|
|
3210
|
+
cleanupErr
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3111
3213
|
}
|
|
3112
|
-
|
|
3113
|
-
console.error("publishDocument failed (node create auto-publish)", err);
|
|
3114
|
-
published = false;
|
|
3214
|
+
throw err;
|
|
3115
3215
|
}
|
|
3116
3216
|
const node = await storage.readDocument(id);
|
|
3117
3217
|
await syncNodeTags(nestId, id, tags);
|
|
@@ -3126,7 +3226,8 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3126
3226
|
author: userEmail,
|
|
3127
3227
|
status: "published",
|
|
3128
3228
|
tags,
|
|
3129
|
-
changeNote: publishNote
|
|
3229
|
+
changeNote: publishNote,
|
|
3230
|
+
client: input.client
|
|
3130
3231
|
});
|
|
3131
3232
|
await setApprovedVersion(nestId, id, savedVersion, userEmail);
|
|
3132
3233
|
} else {
|
|
@@ -3134,10 +3235,12 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3134
3235
|
nestId,
|
|
3135
3236
|
nodeId: id,
|
|
3136
3237
|
version: 1,
|
|
3137
|
-
|
|
3238
|
+
// A pdf node's body is what the engine extracted, not anything sent.
|
|
3239
|
+
content: input.pdf ? node.body || "" : input.content,
|
|
3138
3240
|
author: userEmail,
|
|
3139
3241
|
status: "draft",
|
|
3140
|
-
tags
|
|
3242
|
+
tags,
|
|
3243
|
+
client: input.client
|
|
3141
3244
|
});
|
|
3142
3245
|
}
|
|
3143
3246
|
await trackEvent("node.create", { nestId, nodeId: id });
|
|
@@ -3191,7 +3294,7 @@ async function alignOnDiskStatus(nestId, documentId, status) {
|
|
|
3191
3294
|
);
|
|
3192
3295
|
}
|
|
3193
3296
|
}
|
|
3194
|
-
async function registerImportedDocuments(nestId, userEmail) {
|
|
3297
|
+
async function registerImportedDocuments(nestId, userEmail, client) {
|
|
3195
3298
|
const startedAt = Date.now();
|
|
3196
3299
|
const tracked = await getTrackedNodeIds(nestId);
|
|
3197
3300
|
(await engineCache.get(nestId)).invalidateDiscovery();
|
|
@@ -3221,7 +3324,8 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
3221
3324
|
author: userEmail,
|
|
3222
3325
|
status: d.status,
|
|
3223
3326
|
changeNote: d.status === "published" ? "Imported from existing folder" : "Imported from existing folder \u2014 could not be published, kept as a draft",
|
|
3224
|
-
tags
|
|
3327
|
+
tags,
|
|
3328
|
+
client
|
|
3225
3329
|
});
|
|
3226
3330
|
if (d.status === "published") {
|
|
3227
3331
|
await setApprovedVersion(nestId, d.id, d.version, userEmail);
|
|
@@ -3243,6 +3347,44 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
3243
3347
|
await trackEvent("nest.import.documents", { nestId, registered: docs.length });
|
|
3244
3348
|
return docs.length;
|
|
3245
3349
|
}
|
|
3350
|
+
async function stagePdfImport(nestId, nodeId, pdf, userEmail, client, priorRaw) {
|
|
3351
|
+
const { storage } = await engineCache.get(nestId);
|
|
3352
|
+
const sidecar = pdfSidecarPath(nodeId);
|
|
3353
|
+
let priorSidecar = null;
|
|
3354
|
+
try {
|
|
3355
|
+
priorSidecar = await storage.readVaultBinary(sidecar);
|
|
3356
|
+
} catch {
|
|
3357
|
+
}
|
|
3358
|
+
const sealedBefore = await storage.maxRecordedVersion(nodeId);
|
|
3359
|
+
const rollback = async () => {
|
|
3360
|
+
if (await storage.maxRecordedVersion(nodeId).catch(() => sealedBefore) > sealedBefore) {
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
try {
|
|
3364
|
+
await storage.writeDocument(nodeId, priorRaw);
|
|
3365
|
+
if (priorSidecar) await storage.writeVaultBinary(sidecar, priorSidecar);
|
|
3366
|
+
await storage.regenerateIndex();
|
|
3367
|
+
} catch (err) {
|
|
3368
|
+
console.error("[pdf] could not roll back a failed PDF replace", nestId, nodeId, err);
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
try {
|
|
3372
|
+
const result = await engineApi.run(
|
|
3373
|
+
"context_import_pdf",
|
|
3374
|
+
{
|
|
3375
|
+
id: nodeId,
|
|
3376
|
+
bytes_base64: Buffer.from(pdf.bytes).toString("base64"),
|
|
3377
|
+
...pdf.filename ? { filename: pdf.filename } : {},
|
|
3378
|
+
publish: false,
|
|
3379
|
+
...client ? { client } : {}
|
|
3380
|
+
},
|
|
3381
|
+
await opContext(nestId, userEmail)
|
|
3382
|
+
);
|
|
3383
|
+
return { unchanged: result.unchanged === true, rollback };
|
|
3384
|
+
} catch (err) {
|
|
3385
|
+
throw pdfEngineError(err);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3246
3388
|
async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
3247
3389
|
return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
|
|
3248
3390
|
try {
|
|
@@ -3255,6 +3397,35 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3255
3397
|
} catch {
|
|
3256
3398
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
3257
3399
|
}
|
|
3400
|
+
const priorRaw = node.rawContent;
|
|
3401
|
+
let pdfRollback = null;
|
|
3402
|
+
if (patch.type === void 0 || patch.type === node.frontmatter.type) {
|
|
3403
|
+
assertUpdatableType(node.frontmatter.type);
|
|
3404
|
+
}
|
|
3405
|
+
const storedType = node.frontmatter.type || "document";
|
|
3406
|
+
const isPdfNode = storedType === PDF_NODE_TYPE;
|
|
3407
|
+
if (patch.pdf && !isPdfNode) {
|
|
3408
|
+
throw new ValidationError(
|
|
3409
|
+
`${nodeId} is a "${storedType}" node, not a PDF \u2014 a PDF can only replace the PDF of a pdf node.`
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
if (patch.type !== void 0 && patch.type !== storedType) {
|
|
3413
|
+
if (isPdfNode || patch.type === PDF_NODE_TYPE) {
|
|
3414
|
+
throw new ValidationError(
|
|
3415
|
+
isPdfNode ? "A PDF document can't be converted to another type \u2014 its body is the text of its PDF." : `A document can't be converted to a PDF \u2014 ${PDF_UPLOAD_HINT}.`
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
if (isPdfNode) {
|
|
3420
|
+
const readOnly = "This is a PDF document: its text is extracted from the PDF and can't be edited directly. Upload a new version of the PDF (Replace PDF) to change it.";
|
|
3421
|
+
if (patch.append) throw new ConflictError(readOnly);
|
|
3422
|
+
if (patch.content !== void 0) {
|
|
3423
|
+
if (patch.content.trim() !== (node.body || "").trim()) {
|
|
3424
|
+
throw new ConflictError(readOnly);
|
|
3425
|
+
}
|
|
3426
|
+
patch = { ...patch, content: void 0 };
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3258
3429
|
const previousBody = node.body || "";
|
|
3259
3430
|
const opInput = {};
|
|
3260
3431
|
if (patch.content !== void 0) {
|
|
@@ -3350,11 +3521,30 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3350
3521
|
if (publishReason === null || isAuthorEditingOwnReview) {
|
|
3351
3522
|
const currentVersion = await getCurrentVersion(nestId, nodeId);
|
|
3352
3523
|
const newVersion = await versionManager.nextVersion(nodeId, currentVersion);
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3524
|
+
if (patch.pdf) {
|
|
3525
|
+
const staged = await stagePdfImport(
|
|
3526
|
+
nestId,
|
|
3527
|
+
nodeId,
|
|
3528
|
+
patch.pdf,
|
|
3529
|
+
userEmail,
|
|
3530
|
+
patch.client,
|
|
3531
|
+
priorRaw
|
|
3532
|
+
);
|
|
3533
|
+
if (staged.unchanged && Object.keys(opInput).length === 0) {
|
|
3534
|
+
return { node, version: node.frontmatter.version || currentVersion || 1 };
|
|
3535
|
+
}
|
|
3536
|
+
if (!staged.unchanged) pdfRollback = staged.rollback;
|
|
3537
|
+
}
|
|
3538
|
+
try {
|
|
3539
|
+
await engineApi.run(
|
|
3540
|
+
"context_update",
|
|
3541
|
+
{ id: nodeId, ...opInput, publish: false, version: newVersion },
|
|
3542
|
+
await opContext(nestId, userEmail)
|
|
3543
|
+
);
|
|
3544
|
+
} catch (err) {
|
|
3545
|
+
if (pdfRollback) await pdfRollback();
|
|
3546
|
+
throw err;
|
|
3547
|
+
}
|
|
3358
3548
|
node = await storage.readDocument(nodeId);
|
|
3359
3549
|
await syncNodeTags(nestId, nodeId, currentTags);
|
|
3360
3550
|
await deleteSupersededDrafts(nestId, nodeId, newVersion);
|
|
@@ -3366,7 +3556,8 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3366
3556
|
author: userEmail,
|
|
3367
3557
|
status: "draft",
|
|
3368
3558
|
tags: currentTags,
|
|
3369
|
-
changeNote: patch.changeNote
|
|
3559
|
+
changeNote: patch.changeNote,
|
|
3560
|
+
client: patch.client
|
|
3370
3561
|
});
|
|
3371
3562
|
if (isAuthorEditingOwnReview) {
|
|
3372
3563
|
await repointPendingReview({ nestId, nodeId, newVersion });
|
|
@@ -3375,6 +3566,20 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3375
3566
|
} else {
|
|
3376
3567
|
const publishNote = patch.changeNote || `Auto-published on edit (${publishReason})`;
|
|
3377
3568
|
let publishedVersion;
|
|
3569
|
+
if (patch.pdf) {
|
|
3570
|
+
const staged = await stagePdfImport(
|
|
3571
|
+
nestId,
|
|
3572
|
+
nodeId,
|
|
3573
|
+
patch.pdf,
|
|
3574
|
+
userEmail,
|
|
3575
|
+
patch.client,
|
|
3576
|
+
priorRaw
|
|
3577
|
+
);
|
|
3578
|
+
if (staged.unchanged && Object.keys(opInput).length === 0) {
|
|
3579
|
+
return { node, version: node.frontmatter.version || 1 };
|
|
3580
|
+
}
|
|
3581
|
+
if (!staged.unchanged) pdfRollback = staged.rollback;
|
|
3582
|
+
}
|
|
3378
3583
|
try {
|
|
3379
3584
|
const result = await engineApi.run(
|
|
3380
3585
|
"context_update",
|
|
@@ -3387,6 +3592,7 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3387
3592
|
"publishDocument failed (node patch auto-publish)",
|
|
3388
3593
|
err
|
|
3389
3594
|
);
|
|
3595
|
+
if (pdfRollback) await pdfRollback();
|
|
3390
3596
|
throw err;
|
|
3391
3597
|
}
|
|
3392
3598
|
node = await storage.readDocument(nodeId);
|
|
@@ -3399,7 +3605,8 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3399
3605
|
author: userEmail,
|
|
3400
3606
|
status: "published",
|
|
3401
3607
|
tags: currentTags,
|
|
3402
|
-
changeNote: publishNote
|
|
3608
|
+
changeNote: publishNote,
|
|
3609
|
+
client: patch.client
|
|
3403
3610
|
});
|
|
3404
3611
|
await setApprovedVersion(
|
|
3405
3612
|
nestId,
|
|
@@ -3505,6 +3712,11 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
3505
3712
|
} catch {
|
|
3506
3713
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
3507
3714
|
}
|
|
3715
|
+
if (node.frontmatter.type === PDF_NODE_TYPE) {
|
|
3716
|
+
throw new ValidationError(
|
|
3717
|
+
"PDF documents can't be moved to another folder yet. Upload the PDF again in the destination folder instead."
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
3508
3720
|
await assertNotReviewLocked(nestId, nodeId, userEmail, "moving");
|
|
3509
3721
|
const leaf = nodeId.split("/").pop();
|
|
3510
3722
|
const segments = folderToSegments(folder);
|
|
@@ -3601,6 +3813,16 @@ async function discardDraft(nestId, nodeId, userEmail) {
|
|
|
3601
3813
|
await storage.writeDocument(nodeId, raw);
|
|
3602
3814
|
await storage.regenerateIndex();
|
|
3603
3815
|
const restored = parseDocument(`${nodeId}.md`, raw, nodeId);
|
|
3816
|
+
let warning;
|
|
3817
|
+
if (restored.frontmatter.type === PDF_NODE_TYPE) {
|
|
3818
|
+
try {
|
|
3819
|
+
const bytes = await readPdfBinary(storage, nodeId);
|
|
3820
|
+
await storage.writeVaultBinary(pdfSidecarPath(nodeId), bytes);
|
|
3821
|
+
} catch (err) {
|
|
3822
|
+
console.error("[pdf] could not restore the sealed binary on discard", nestId, nodeId, err);
|
|
3823
|
+
warning = `The draft was discarded, but the PDF of version ${sealed} could not be restored (its archived copy is missing or no longer matches). Upload the PDF again, or ask an admin to verify the nest.`;
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3604
3826
|
await syncNodeTags(
|
|
3605
3827
|
nestId,
|
|
3606
3828
|
nodeId,
|
|
@@ -3608,7 +3830,7 @@ async function discardDraft(nestId, nodeId, userEmail) {
|
|
|
3608
3830
|
);
|
|
3609
3831
|
await deleteUnsealedVersions(nestId, nodeId, sealed);
|
|
3610
3832
|
await trackEvent("node.discard_draft", { nestId, nodeId, version: sealed });
|
|
3611
|
-
return { deleted: false, version: sealed };
|
|
3833
|
+
return { deleted: false, version: sealed, ...warning ? { warning } : {} };
|
|
3612
3834
|
});
|
|
3613
3835
|
}
|
|
3614
3836
|
async function deleteNode(nestId, nodeId, userEmail) {
|
|
@@ -3835,7 +4057,7 @@ function collectMarkdownFiles(dir, root) {
|
|
|
3835
4057
|
}
|
|
3836
4058
|
return out;
|
|
3837
4059
|
}
|
|
3838
|
-
async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
4060
|
+
async function syncUnsyncedFolder(userId, folderName, callerEmail, client) {
|
|
3839
4061
|
console.log(
|
|
3840
4062
|
`[unsynced] sync requested folder="${folderName}" user="${callerEmail}"`
|
|
3841
4063
|
);
|
|
@@ -3876,7 +4098,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3876
4098
|
let documents;
|
|
3877
4099
|
try {
|
|
3878
4100
|
await stageImportedFiles(nest.id, files, callerEmail);
|
|
3879
|
-
documents = await registerImportedDocuments(nest.id, callerEmail);
|
|
4101
|
+
documents = await registerImportedDocuments(nest.id, callerEmail, client);
|
|
3880
4102
|
} catch (err) {
|
|
3881
4103
|
await deleteNest(nest.id).catch(() => {
|
|
3882
4104
|
});
|
|
@@ -3997,7 +4219,11 @@ nestRoutes.get("/", async (c) => {
|
|
|
3997
4219
|
seen.add(n.id);
|
|
3998
4220
|
deduped.push(n);
|
|
3999
4221
|
}
|
|
4000
|
-
const
|
|
4222
|
+
const memberIds = new Set([...owned, ...shared].map((n) => n.id));
|
|
4223
|
+
const out = (await Promise.all(deduped.map(annotate))).map((row) => ({
|
|
4224
|
+
...row,
|
|
4225
|
+
discovered: !memberIds.has(row.id)
|
|
4226
|
+
}));
|
|
4001
4227
|
const docRows = await db.all(
|
|
4002
4228
|
`SELECT nest_id,
|
|
4003
4229
|
COUNT(DISTINCT CASE
|
|
@@ -4096,7 +4322,14 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4096
4322
|
}
|
|
4097
4323
|
const email2 = await resolveCallerEmail(userId);
|
|
4098
4324
|
if (body.finalize) {
|
|
4099
|
-
return c.json({
|
|
4325
|
+
return c.json({
|
|
4326
|
+
nest: nest2,
|
|
4327
|
+
documents: await registerImportedDocuments(
|
|
4328
|
+
target,
|
|
4329
|
+
email2,
|
|
4330
|
+
attributeRequest(c, body.client)
|
|
4331
|
+
)
|
|
4332
|
+
});
|
|
4100
4333
|
}
|
|
4101
4334
|
const registered = await getDb().get(
|
|
4102
4335
|
"SELECT COUNT(*) AS c FROM node_versions WHERE nest_id = ?",
|
|
@@ -4123,7 +4356,11 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4123
4356
|
if (body.partial) {
|
|
4124
4357
|
return c.json({ nest, rejected }, 201);
|
|
4125
4358
|
}
|
|
4126
|
-
const documents = await registerImportedDocuments(
|
|
4359
|
+
const documents = await registerImportedDocuments(
|
|
4360
|
+
nest.id,
|
|
4361
|
+
email,
|
|
4362
|
+
attributeRequest(c, body.client)
|
|
4363
|
+
);
|
|
4127
4364
|
return c.json({ nest, documents, rejected }, 201);
|
|
4128
4365
|
} catch (err) {
|
|
4129
4366
|
await deleteNest(nest.id).catch(() => {
|
|
@@ -4153,7 +4390,8 @@ nestRoutes.post("/:nestId/sync", async (c) => {
|
|
|
4153
4390
|
const result = await syncUnsyncedFolder(
|
|
4154
4391
|
userId,
|
|
4155
4392
|
body.name,
|
|
4156
|
-
await resolveCallerEmail(userId)
|
|
4393
|
+
await resolveCallerEmail(userId),
|
|
4394
|
+
attributeRequest(c, body.client)
|
|
4157
4395
|
);
|
|
4158
4396
|
return c.json(result, 201);
|
|
4159
4397
|
});
|
|
@@ -4275,7 +4513,8 @@ nestRoutes.get("/:nestId/settings", async (c) => {
|
|
|
4275
4513
|
stewardship_enabled: await isStewardshipEnabled(nestId),
|
|
4276
4514
|
allow_self_approve: await nestAllowsSelfApprove(nestId),
|
|
4277
4515
|
prime_only_review: await nestReviewsPrimeOnly(nestId),
|
|
4278
|
-
prime_tags: await nestPrimeTags(nestId)
|
|
4516
|
+
prime_tags: await nestPrimeTags(nestId),
|
|
4517
|
+
creator_is_reviewer: await nestCreatorIsReviewer(nestId)
|
|
4279
4518
|
});
|
|
4280
4519
|
});
|
|
4281
4520
|
nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
@@ -4320,11 +4559,15 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
4320
4559
|
Array.isArray(body.prime_tags) ? body.prime_tags : [body.prime_tags]
|
|
4321
4560
|
);
|
|
4322
4561
|
}
|
|
4562
|
+
if (typeof body.creator_is_reviewer === "boolean") {
|
|
4563
|
+
await setCreatorIsReviewer(nestId, body.creator_is_reviewer);
|
|
4564
|
+
}
|
|
4323
4565
|
return c.json({
|
|
4324
4566
|
stewardship_enabled: await isStewardshipEnabled(nestId),
|
|
4325
4567
|
allow_self_approve: await nestAllowsSelfApprove(nestId),
|
|
4326
4568
|
prime_only_review: await nestReviewsPrimeOnly(nestId),
|
|
4327
4569
|
prime_tags: await nestPrimeTags(nestId),
|
|
4570
|
+
creator_is_reviewer: await nestCreatorIsReviewer(nestId),
|
|
4328
4571
|
wiped
|
|
4329
4572
|
});
|
|
4330
4573
|
});
|
|
@@ -4539,12 +4782,24 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
4539
4782
|
if (!body.visibility || !["private", "org", "public"].includes(body.visibility)) {
|
|
4540
4783
|
throw new ValidationError("visibility must be private, org or public");
|
|
4541
4784
|
}
|
|
4542
|
-
|
|
4785
|
+
const ack = body.acknowledge_public === true;
|
|
4786
|
+
const shared = config.SHARED_DEPLOYMENT;
|
|
4787
|
+
if (body.visibility === "public" && !ack) {
|
|
4543
4788
|
return c.json(
|
|
4544
4789
|
{
|
|
4545
4790
|
error: "Publishing this nest requires acknowledgement.",
|
|
4546
4791
|
requires_acknowledgement: true,
|
|
4547
|
-
warning: "This makes the nest readable by anyone on the internet, outside your organization."
|
|
4792
|
+
warning: shared ? "This makes the nest readable by anyone on the internet \u2014 including every account on this server, which belongs to other organizations too." : "This makes the nest readable by anyone on the internet, outside your organization."
|
|
4793
|
+
},
|
|
4794
|
+
409
|
|
4795
|
+
);
|
|
4796
|
+
}
|
|
4797
|
+
if (body.visibility === "org" && shared && !ack) {
|
|
4798
|
+
return c.json(
|
|
4799
|
+
{
|
|
4800
|
+
error: "Opening this nest to the organization requires acknowledgement.",
|
|
4801
|
+
requires_acknowledgement: true,
|
|
4802
|
+
warning: "This server is shared: 'Organization' makes the nest readable by every account on this server, including other organizations \u2014 not just yours."
|
|
4548
4803
|
},
|
|
4549
4804
|
409
|
|
4550
4805
|
);
|
|
@@ -5338,7 +5593,7 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
5338
5593
|
tags: ["annotations"]
|
|
5339
5594
|
},
|
|
5340
5595
|
userEmail,
|
|
5341
|
-
{ notifyMentions: false }
|
|
5596
|
+
{ notifyMentions: false, creatorReviews: false }
|
|
5342
5597
|
);
|
|
5343
5598
|
} else {
|
|
5344
5599
|
throw err;
|
|
@@ -5504,7 +5759,7 @@ var GATE_EYE_SCRIPT = `<script>
|
|
|
5504
5759
|
</script>`;
|
|
5505
5760
|
function gatePage(row, message) {
|
|
5506
5761
|
const wantsCode = row.access_mode === "code";
|
|
5507
|
-
const
|
|
5762
|
+
const field2 = wantsCode ? `<label>Access code<span class="wrap">
|
|
5508
5763
|
<input id="cn-code" name="code" type="password" autocomplete="off" required autofocus>
|
|
5509
5764
|
<button type="button" id="cn-eye" class="eye" aria-label="Show code" aria-pressed="false">${EYE_SVG}</button>
|
|
5510
5765
|
</span></label>` : `<label>Your email<input name="email" type="email" autocomplete="email" required autofocus></label>`;
|
|
@@ -5528,7 +5783,7 @@ function gatePage(row, message) {
|
|
|
5528
5783
|
.err{color:#be123c;font-weight:600}
|
|
5529
5784
|
.brand{font-size:12px;color:#a8a29e;margin:18px 0 0;text-align:center}
|
|
5530
5785
|
</style></head><body><form class="card" method="POST" action="/p/${esc(row.slug)}/gate">
|
|
5531
|
-
<h1>Protected document</h1><p>${esc(note)}</p>${err}${
|
|
5786
|
+
<h1>Protected document</h1><p>${esc(note)}</p>${err}${field2}
|
|
5532
5787
|
<button type="submit">${cta}</button>
|
|
5533
5788
|
<p class="brand">Secured by PromptOwl \xB7 ContextNest</p>
|
|
5534
5789
|
</form>${wantsCode ? GATE_EYE_SCRIPT : ""}</body></html>`;
|
|
@@ -6108,6 +6363,7 @@ function toSearchHit(node, query) {
|
|
|
6108
6363
|
|
|
6109
6364
|
// src/nodes/routes.ts
|
|
6110
6365
|
import { Hono as Hono11 } from "hono";
|
|
6366
|
+
import { parseDocument as parseDocument3, readPdfBinary as readPdfBinary2 } from "@promptowl/contextnest-engine";
|
|
6111
6367
|
|
|
6112
6368
|
// src/nodes/markdown-export.ts
|
|
6113
6369
|
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
@@ -6224,7 +6480,8 @@ nodeRoutes.post("/", async (c) => {
|
|
|
6224
6480
|
status: body.status,
|
|
6225
6481
|
schedule: body.schedule,
|
|
6226
6482
|
folder: body.folder,
|
|
6227
|
-
prime: body.prime
|
|
6483
|
+
prime: body.prime,
|
|
6484
|
+
client: attributeRequest(c, body.client)
|
|
6228
6485
|
},
|
|
6229
6486
|
authorEmail
|
|
6230
6487
|
);
|
|
@@ -6241,7 +6498,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
6241
6498
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
6242
6499
|
const nestId = c.req.param("nestId");
|
|
6243
6500
|
const nodeId = c.req.param("nodeId");
|
|
6244
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
6501
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-S22V2JKG.js");
|
|
6245
6502
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
6246
6503
|
nestId,
|
|
6247
6504
|
nodeId
|
|
@@ -6262,7 +6519,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
6262
6519
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
6263
6520
|
const nestId = c.req.param("nestId");
|
|
6264
6521
|
const nodeId = c.req.param("nodeId");
|
|
6265
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
6522
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-4G2XUFS6.js");
|
|
6266
6523
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
6267
6524
|
return c.json({ reviews: history });
|
|
6268
6525
|
});
|
|
@@ -6283,7 +6540,7 @@ nodeRoutes.post("/:nodeId{.+}/discard", async (c) => {
|
|
|
6283
6540
|
nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
6284
6541
|
const nestId = c.req.param("nestId");
|
|
6285
6542
|
const nodeId = c.req.param("nodeId");
|
|
6286
|
-
const { versions: versionManager } = await engineCache.get(nestId);
|
|
6543
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
6287
6544
|
const userId = c.get("userId");
|
|
6288
6545
|
const userEmail = await resolveCallerEmail(userId);
|
|
6289
6546
|
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
@@ -6305,11 +6562,26 @@ nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
|
6305
6562
|
`Version ${targetVersion} not found for ${nodeId}`
|
|
6306
6563
|
);
|
|
6307
6564
|
}
|
|
6565
|
+
let restoredPdf = null;
|
|
6566
|
+
if (pdfMetaOf(nodeId, raw)) {
|
|
6567
|
+
try {
|
|
6568
|
+
restoredPdf = await readPdfBinary2(storage, nodeId, {
|
|
6569
|
+
version: targetVersion
|
|
6570
|
+
});
|
|
6571
|
+
} catch (err) {
|
|
6572
|
+
console.error("[pdf] revert could not read the version's binary", nestId, nodeId, targetVersion, err);
|
|
6573
|
+
throw new NotFoundError(`The PDF of version ${targetVersion} is not available to restore.`);
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6308
6576
|
const content = bodyOnly(nodeId, raw);
|
|
6309
6577
|
const { node, version } = await updateNode(
|
|
6310
6578
|
nestId,
|
|
6311
6579
|
nodeId,
|
|
6312
|
-
{
|
|
6580
|
+
{
|
|
6581
|
+
...restoredPdf ? { pdf: { bytes: new Uint8Array(restoredPdf) } } : { content },
|
|
6582
|
+
changeNote: `Restored from version ${targetVersion}`,
|
|
6583
|
+
client: attributeRequest(c, body.client)
|
|
6584
|
+
},
|
|
6313
6585
|
userEmail
|
|
6314
6586
|
);
|
|
6315
6587
|
await trackEvent("node.revert", { nestId, nodeId, targetVersion });
|
|
@@ -6412,6 +6684,10 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
6412
6684
|
approved
|
|
6413
6685
|
);
|
|
6414
6686
|
response.content = bodyOnly(nodeId, approvedRaw);
|
|
6687
|
+
if (response.type === "pdf") {
|
|
6688
|
+
const approvedPdf = pdfMetaOf(nodeId, approvedRaw);
|
|
6689
|
+
if (approvedPdf) response.pdf = approvedPdf;
|
|
6690
|
+
}
|
|
6415
6691
|
} catch (err) {
|
|
6416
6692
|
console.error(
|
|
6417
6693
|
"reconstructVersion failed (public single)",
|
|
@@ -6494,6 +6770,7 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
6494
6770
|
changeNote: body.changeNote,
|
|
6495
6771
|
schedule: body.schedule,
|
|
6496
6772
|
prime: body.prime,
|
|
6773
|
+
client: attributeRequest(c, body.client),
|
|
6497
6774
|
type: body.type
|
|
6498
6775
|
},
|
|
6499
6776
|
authorEmail
|
|
@@ -6506,6 +6783,13 @@ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
|
|
|
6506
6783
|
await deleteNode(nestId, nodeId, await getUserEmail(c));
|
|
6507
6784
|
return c.json({ deleted: true });
|
|
6508
6785
|
});
|
|
6786
|
+
function pdfMetaOf(nodeId, raw) {
|
|
6787
|
+
try {
|
|
6788
|
+
return parseDocument3(`${nodeId}.md`, raw, nodeId).frontmatter.pdf;
|
|
6789
|
+
} catch {
|
|
6790
|
+
return void 0;
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6509
6793
|
async function getUserEmail(c) {
|
|
6510
6794
|
const userId = c.get("userId");
|
|
6511
6795
|
const db = getDb();
|
|
@@ -7231,6 +7515,17 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7231
7515
|
}
|
|
7232
7516
|
}
|
|
7233
7517
|
const tags = [...tagCounts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
|
|
7518
|
+
for (const docIds of tagToDocs.values()) {
|
|
7519
|
+
const ids = [...docIds].sort();
|
|
7520
|
+
for (let i = 0; i < ids.length; i++) {
|
|
7521
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
7522
|
+
const key = `${ids[i]}->${ids[j]}`;
|
|
7523
|
+
if (seen.has(key) || seen.has(`${ids[j]}->${ids[i]}`)) continue;
|
|
7524
|
+
seen.add(key);
|
|
7525
|
+
allLinks.push({ source: ids[i], target: ids[j] });
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
}
|
|
7234
7529
|
const exposeStewards = await isStewardshipEnabled(nestId) && !await isPublicReader(nestId, userId);
|
|
7235
7530
|
const stewardRows = exposeStewards ? (await listStewards({ nestId })).filter(
|
|
7236
7531
|
// Drop document-scoped rows whose node the caller can't see.
|
|
@@ -7879,7 +8174,8 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
7879
8174
|
content: doc.content,
|
|
7880
8175
|
type: doc.type,
|
|
7881
8176
|
tags: doc.tags,
|
|
7882
|
-
scope: doc.scope
|
|
8177
|
+
scope: doc.scope,
|
|
8178
|
+
folder: doc.folder
|
|
7883
8179
|
},
|
|
7884
8180
|
userEmail
|
|
7885
8181
|
);
|
|
@@ -7915,6 +8211,9 @@ import { Hono as Hono15 } from "hono";
|
|
|
7915
8211
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7916
8212
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
7917
8213
|
|
|
8214
|
+
// src/mcp/tools.ts
|
|
8215
|
+
import { parseDocument as parseDocument4 } from "@promptowl/contextnest-engine";
|
|
8216
|
+
|
|
7918
8217
|
// src/telemetry/trace-log.ts
|
|
7919
8218
|
var PRUNE_EVERY = 500;
|
|
7920
8219
|
var insertsSincePrune = 0;
|
|
@@ -7923,8 +8222,8 @@ async function logTraceEvent(e) {
|
|
|
7923
8222
|
const db = getDb();
|
|
7924
8223
|
await db.run(
|
|
7925
8224
|
`INSERT INTO api_events
|
|
7926
|
-
(ts, kind, method, path, tool, nest_id, user_id, user_email, status, duration_ms)
|
|
7927
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
8225
|
+
(ts, kind, method, path, tool, nest_id, user_id, user_email, status, duration_ms, client_json)
|
|
8226
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
7928
8227
|
[
|
|
7929
8228
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
7930
8229
|
e.kind,
|
|
@@ -7935,7 +8234,8 @@ async function logTraceEvent(e) {
|
|
|
7935
8234
|
e.userId ?? null,
|
|
7936
8235
|
e.userEmail ?? null,
|
|
7937
8236
|
e.status ?? null,
|
|
7938
|
-
e.durationMs ?? null
|
|
8237
|
+
e.durationMs ?? null,
|
|
8238
|
+
serializeClient(e.client)
|
|
7939
8239
|
]
|
|
7940
8240
|
);
|
|
7941
8241
|
if (++insertsSincePrune >= PRUNE_EVERY) {
|
|
@@ -7984,7 +8284,13 @@ async function listTraceEvents(filters = {}) {
|
|
|
7984
8284
|
LIMIT ? OFFSET ?`,
|
|
7985
8285
|
[...args, limit, offset]
|
|
7986
8286
|
);
|
|
7987
|
-
return {
|
|
8287
|
+
return {
|
|
8288
|
+
events: events.map(({ client_json, ...e }) => {
|
|
8289
|
+
const client = deserializeClient(client_json);
|
|
8290
|
+
return client ? { ...e, client } : e;
|
|
8291
|
+
}),
|
|
8292
|
+
total
|
|
8293
|
+
};
|
|
7988
8294
|
} catch {
|
|
7989
8295
|
return { events: [], total: 0 };
|
|
7990
8296
|
}
|
|
@@ -9032,6 +9338,19 @@ function summarizeNode(node, body) {
|
|
|
9032
9338
|
body: body ?? void 0
|
|
9033
9339
|
};
|
|
9034
9340
|
}
|
|
9341
|
+
function pdfSourceLine(node, body) {
|
|
9342
|
+
if ((node.frontmatter.type || "document") !== "pdf") return null;
|
|
9343
|
+
let pdf = node.frontmatter.pdf;
|
|
9344
|
+
if (body && body.startsWith("---")) {
|
|
9345
|
+
try {
|
|
9346
|
+
pdf = parseDocument4(`${node.id}.md`, body, node.id).frontmatter.pdf ?? pdf;
|
|
9347
|
+
} catch {
|
|
9348
|
+
}
|
|
9349
|
+
}
|
|
9350
|
+
if (!pdf) return "**Source file:** PDF";
|
|
9351
|
+
const pages = `${pdf.pages} page${pdf.pages === 1 ? "" : "s"}`;
|
|
9352
|
+
return `**Source file:** PDF, ${pages}${pdf.text_layer ? "" : ", no text layer"}`;
|
|
9353
|
+
}
|
|
9035
9354
|
var TOOL_DEFINITIONS = [
|
|
9036
9355
|
{
|
|
9037
9356
|
name: "context_init",
|
|
@@ -9262,7 +9581,7 @@ var TOOL_DEFINITIONS = [
|
|
|
9262
9581
|
tags: {
|
|
9263
9582
|
type: "array",
|
|
9264
9583
|
items: { type: "string" },
|
|
9265
|
-
description: "
|
|
9584
|
+
description: "The node's full tag set \u2014 REPLACES the existing tags (same as the engine catalog). To add one, send the current tags plus the new one; to remove one, send the set without it."
|
|
9266
9585
|
},
|
|
9267
9586
|
status: {
|
|
9268
9587
|
type: "string",
|
|
@@ -9431,10 +9750,14 @@ var TOOL_DEFINITIONS = [
|
|
|
9431
9750
|
type: "object",
|
|
9432
9751
|
properties: {
|
|
9433
9752
|
title: { type: "string", description: "Title of the node to submit" },
|
|
9753
|
+
id: {
|
|
9754
|
+
type: "string",
|
|
9755
|
+
description: 'Node id / path (e.g. "nodes/gtm/deals/win-loss-log"). Use instead of title when two nodes share a title or the title is unknown.'
|
|
9756
|
+
},
|
|
9434
9757
|
note: { type: "string", description: "Optional note for the reviewer" },
|
|
9435
9758
|
priority: { type: "string", description: "Priority: low, normal (default), high, urgent" }
|
|
9436
|
-
}
|
|
9437
|
-
|
|
9759
|
+
}
|
|
9760
|
+
// One of id or title — see context_update.
|
|
9438
9761
|
}
|
|
9439
9762
|
},
|
|
9440
9763
|
{
|
|
@@ -9572,8 +9895,30 @@ async function resolveLlmBody(ctx, node, gate) {
|
|
|
9572
9895
|
async function handleToolCall(toolName, args, ctx) {
|
|
9573
9896
|
const started = Date.now();
|
|
9574
9897
|
let status = 200;
|
|
9898
|
+
let client;
|
|
9899
|
+
try {
|
|
9900
|
+
client = mergeClient(ctx.client, parseClientMetadata(args.client));
|
|
9901
|
+
} catch (err) {
|
|
9902
|
+
void logTraceEvent({
|
|
9903
|
+
kind: "mcp",
|
|
9904
|
+
tool: toolName,
|
|
9905
|
+
nestId: ctx.nestId,
|
|
9906
|
+
userId: ctx.userId,
|
|
9907
|
+
userEmail: ctx.userEmail,
|
|
9908
|
+
status: 400,
|
|
9909
|
+
durationMs: Date.now() - started
|
|
9910
|
+
});
|
|
9911
|
+
return toolError(err.message, "VALIDATION_FAILED");
|
|
9912
|
+
}
|
|
9913
|
+
const attributed = { ...ctx, client };
|
|
9914
|
+
if ((toolName === "context_create" || toolName === "context_update") && typeof args.content === "string" && /^\s*---\r?\n[\w-]+:/.test(args.content)) {
|
|
9915
|
+
return toolError(
|
|
9916
|
+
"`content` is the markdown body only \u2014 it starts with a YAML frontmatter block, which would be stored as body text. Drop the `---` block and pass tags/status as parameters; the server owns the frontmatter.",
|
|
9917
|
+
"VALIDATION_FAILED"
|
|
9918
|
+
);
|
|
9919
|
+
}
|
|
9575
9920
|
try {
|
|
9576
|
-
return await runTool(toolName, args,
|
|
9921
|
+
return await runTool(toolName, args, attributed);
|
|
9577
9922
|
} catch (err) {
|
|
9578
9923
|
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
|
|
9579
9924
|
status = 503;
|
|
@@ -9592,7 +9937,10 @@ async function handleToolCall(toolName, args, ctx) {
|
|
|
9592
9937
|
userId: ctx.userId,
|
|
9593
9938
|
userEmail: ctx.userEmail,
|
|
9594
9939
|
status,
|
|
9595
|
-
durationMs: Date.now() - started
|
|
9940
|
+
durationMs: Date.now() - started,
|
|
9941
|
+
// Reads record attribution HERE — the activity trace is this edition's
|
|
9942
|
+
// §9.2 access trace, and a read seals no version to carry it.
|
|
9943
|
+
client
|
|
9596
9944
|
});
|
|
9597
9945
|
}
|
|
9598
9946
|
}
|
|
@@ -9787,7 +10135,8 @@ ${list}`,
|
|
|
9787
10135
|
const meta = [
|
|
9788
10136
|
`**Title:** ${node.frontmatter.title}`,
|
|
9789
10137
|
`**Type:** ${node.frontmatter.type || "document"}`,
|
|
9790
|
-
node.frontmatter.tags?.length ? `**Tags:** ${node.frontmatter.tags.join(" ")}` : null
|
|
10138
|
+
node.frontmatter.tags?.length ? `**Tags:** ${node.frontmatter.tags.join(" ")}` : null,
|
|
10139
|
+
pdfSourceLine(node, body)
|
|
9791
10140
|
].filter(Boolean).join("\n");
|
|
9792
10141
|
return {
|
|
9793
10142
|
text: `${meta}
|
|
@@ -10050,7 +10399,8 @@ ${sections}`;
|
|
|
10050
10399
|
type: args.type,
|
|
10051
10400
|
tags: args.tags,
|
|
10052
10401
|
scope: args.scope,
|
|
10053
|
-
folder: args.folder
|
|
10402
|
+
folder: args.folder,
|
|
10403
|
+
client: ctx.client
|
|
10054
10404
|
},
|
|
10055
10405
|
userEmail
|
|
10056
10406
|
);
|
|
@@ -10078,16 +10428,16 @@ ${sections}`;
|
|
|
10078
10428
|
"UNAUTHORIZED_ACTION"
|
|
10079
10429
|
);
|
|
10080
10430
|
}
|
|
10081
|
-
const mergedTags = args.tags ? [.../* @__PURE__ */ new Set([...node.frontmatter.tags || [], ...args.tags])] : void 0;
|
|
10082
10431
|
const { node: updated, version } = await updateNode(
|
|
10083
10432
|
nestId,
|
|
10084
10433
|
node.id,
|
|
10085
10434
|
{
|
|
10086
10435
|
content: args.content,
|
|
10087
10436
|
append: args.append,
|
|
10088
|
-
tags:
|
|
10437
|
+
tags: args.tags,
|
|
10089
10438
|
status: args.status,
|
|
10090
|
-
changeNote: args.changeNote
|
|
10439
|
+
changeNote: args.changeNote,
|
|
10440
|
+
client: ctx.client
|
|
10091
10441
|
},
|
|
10092
10442
|
userEmail
|
|
10093
10443
|
);
|
|
@@ -10140,10 +10490,11 @@ Use context_request_deletion to ask a nest owner/admin to delete it, with a reas
|
|
|
10140
10490
|
args.folder,
|
|
10141
10491
|
userEmail
|
|
10142
10492
|
);
|
|
10493
|
+
const data = { id: moved.id, previous_id: previousId, title: moved.frontmatter.title };
|
|
10143
10494
|
if (previousId === moved.id) {
|
|
10144
|
-
return `**${moved.frontmatter.title}** is already at \`${moved.id}\` \u2014 nothing to move
|
|
10495
|
+
return { text: `**${moved.frontmatter.title}** is already at \`${moved.id}\` \u2014 nothing to move.`, data };
|
|
10145
10496
|
}
|
|
10146
|
-
return `Moved **${moved.frontmatter.title}**: \`${previousId}\` \u2192 \`${moved.id}
|
|
10497
|
+
return { text: `Moved **${moved.frontmatter.title}**: \`${previousId}\` \u2192 \`${moved.id}\``, data };
|
|
10147
10498
|
}
|
|
10148
10499
|
// ─── Governance Tool Handlers ──────────────────────────────────────
|
|
10149
10500
|
case "context_stewards": {
|
|
@@ -10386,7 +10737,8 @@ Submit one with \`context_submit_review\`.`;
|
|
|
10386
10737
|
requestedBy: ctx.userEmail,
|
|
10387
10738
|
note: args.note,
|
|
10388
10739
|
priority: args.priority,
|
|
10389
|
-
baseUrl: ctx.baseUrl
|
|
10740
|
+
baseUrl: ctx.baseUrl,
|
|
10741
|
+
client: ctx.client
|
|
10390
10742
|
});
|
|
10391
10743
|
const resolved2 = await resolveStewardsForNode(
|
|
10392
10744
|
ctx.nestId,
|
|
@@ -10437,7 +10789,8 @@ ${resolved2.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` :
|
|
|
10437
10789
|
nodeId: node.id,
|
|
10438
10790
|
approvedBy: ctx.userEmail,
|
|
10439
10791
|
note: args.note,
|
|
10440
|
-
baseUrl: ctx.baseUrl
|
|
10792
|
+
baseUrl: ctx.baseUrl,
|
|
10793
|
+
client: ctx.client
|
|
10441
10794
|
});
|
|
10442
10795
|
return `Approved "${args.title}" v${request.version}. This version is now live for AI queries.${args.note ? `
|
|
10443
10796
|
Note: ${args.note}` : ""}`;
|
|
@@ -10478,7 +10831,9 @@ Reason: ${args.note}`;
|
|
|
10478
10831
|
};
|
|
10479
10832
|
}
|
|
10480
10833
|
const list = allVersions.map(
|
|
10481
|
-
(v) => `- **v${v.version}** ${v.status}${v.version === approved ? " (AI-active)" : ""} \u2014 by ${v.editedBy} at ${v.editedAt}${v.changeNote ? ` "${v.changeNote}"` : ""}
|
|
10834
|
+
(v) => `- **v${v.version}** ${v.status}${v.version === approved ? " (AI-active)" : ""} \u2014 by ${v.editedBy} at ${v.editedAt}${v.changeNote ? ` "${v.changeNote}"` : ""}${// §9.4: which agent wrote it. In the prose as well as the
|
|
10835
|
+
// structured payload — a chat client reads only this half.
|
|
10836
|
+
v.client?.agent ? ` [via ${v.client.agent}]` : ""}`
|
|
10482
10837
|
).join("\n");
|
|
10483
10838
|
return {
|
|
10484
10839
|
text: `# Version History: ${label}
|
|
@@ -10502,7 +10857,11 @@ ${list}`,
|
|
|
10502
10857
|
status: v.status,
|
|
10503
10858
|
edited_by: v.editedBy,
|
|
10504
10859
|
edited_at: v.editedAt,
|
|
10505
|
-
note: v.changeNote
|
|
10860
|
+
note: v.changeNote,
|
|
10861
|
+
// §9.4: which agent wrote this version, in which session. Omitted
|
|
10862
|
+
// (not `{}`) when the write carried no attribution — "not
|
|
10863
|
+
// attributed" and "attributed to nobody" are different claims.
|
|
10864
|
+
...v.client ? { client: v.client } : {}
|
|
10506
10865
|
}))
|
|
10507
10866
|
}
|
|
10508
10867
|
};
|
|
@@ -10563,8 +10922,8 @@ ${list}`,
|
|
|
10563
10922
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
10564
10923
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
10565
10924
|
try {
|
|
10566
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
10567
|
-
const db = (await import("./client-
|
|
10925
|
+
const { createGrant: createGrant2 } = await import("./grants-service-2OTFS7EH.js");
|
|
10926
|
+
const db = (await import("./client-D44ZHDTJ.js")).getDb();
|
|
10568
10927
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
10569
10928
|
const e = normalizeEmail2(String(args.email || ""));
|
|
10570
10929
|
if (!e) return "email is required.";
|
|
@@ -10613,7 +10972,8 @@ ${list}`;
|
|
|
10613
10972
|
const { nest, documents } = await syncUnsyncedFolder(
|
|
10614
10973
|
userId,
|
|
10615
10974
|
args.name,
|
|
10616
|
-
userEmail
|
|
10975
|
+
userEmail,
|
|
10976
|
+
ctx.client
|
|
10617
10977
|
);
|
|
10618
10978
|
return `Synced folder "${args.name}" \u2192 nest **${nest.name}** (${nest.id}) with ${documents} document(s).`;
|
|
10619
10979
|
} catch (err) {
|
|
@@ -10636,7 +10996,7 @@ async function getUserEmail2(userId) {
|
|
|
10636
10996
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
10637
10997
|
return user?.email || "anonymous@localhost";
|
|
10638
10998
|
}
|
|
10639
|
-
function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
10999
|
+
function createMcpServerForNest(nestId, userId, userEmail, baseUrl, c) {
|
|
10640
11000
|
const server = new McpServer(
|
|
10641
11001
|
{ name: `contextnest-${nestId}`, version: "1.0.0" },
|
|
10642
11002
|
{ capabilities: { tools: {} } }
|
|
@@ -10647,16 +11007,18 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
|
10647
11007
|
const required = tool.inputSchema.required || [];
|
|
10648
11008
|
const shape = {};
|
|
10649
11009
|
for (const [key, def] of Object.entries(props)) {
|
|
10650
|
-
let
|
|
10651
|
-
if (def.type === "string")
|
|
10652
|
-
else if (def.type === "number")
|
|
10653
|
-
else if (def.type === "array")
|
|
10654
|
-
else
|
|
10655
|
-
if (!required.includes(key))
|
|
10656
|
-
shape[key] =
|
|
10657
|
-
}
|
|
11010
|
+
let field2;
|
|
11011
|
+
if (def.type === "string") field2 = z.string();
|
|
11012
|
+
else if (def.type === "number") field2 = z.number();
|
|
11013
|
+
else if (def.type === "array") field2 = z.array(z.string());
|
|
11014
|
+
else field2 = z.any();
|
|
11015
|
+
if (!required.includes(key)) field2 = field2.optional();
|
|
11016
|
+
shape[key] = field2;
|
|
11017
|
+
}
|
|
11018
|
+
Object.assign(shape, clientToolField);
|
|
10658
11019
|
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
10659
11020
|
const engine = await engineCache.get(nestId);
|
|
11021
|
+
const client = attributeRequest(c, void 0, connectionClient(server));
|
|
10660
11022
|
const out = await handleToolCall(tool.name, args, {
|
|
10661
11023
|
storage: engine.storage,
|
|
10662
11024
|
queryEngine: engine.query,
|
|
@@ -10664,7 +11026,8 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
|
10664
11026
|
nestId,
|
|
10665
11027
|
userId,
|
|
10666
11028
|
userEmail,
|
|
10667
|
-
baseUrl
|
|
11029
|
+
baseUrl,
|
|
11030
|
+
client
|
|
10668
11031
|
});
|
|
10669
11032
|
return toCallToolResult(out);
|
|
10670
11033
|
});
|
|
@@ -10679,7 +11042,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
10679
11042
|
nestId,
|
|
10680
11043
|
userId,
|
|
10681
11044
|
userEmail,
|
|
10682
|
-
requestBaseUrl(c.req.url)
|
|
11045
|
+
requestBaseUrl(c.req.url),
|
|
11046
|
+
c
|
|
10683
11047
|
);
|
|
10684
11048
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
10685
11049
|
sessionIdGenerator: void 0,
|
|
@@ -11749,8 +12113,185 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
11749
12113
|
});
|
|
11750
12114
|
});
|
|
11751
12115
|
|
|
11752
|
-
// src/
|
|
12116
|
+
// src/nodes/pdf-routes.ts
|
|
11753
12117
|
import { Hono as Hono22 } from "hono";
|
|
12118
|
+
import { bodyLimit } from "hono/body-limit";
|
|
12119
|
+
import { createHash as createHash3 } from "crypto";
|
|
12120
|
+
import { isPdf, readPdfBinary as readPdfBinary3 } from "@promptowl/contextnest-engine";
|
|
12121
|
+
var pdfNodeRoutes = new Hono22();
|
|
12122
|
+
var PDF_UPLOAD_PATH = /^\/nests\/[^/]+\/nodes\/pdf\/?$/;
|
|
12123
|
+
function pdfMaxRequestBytes() {
|
|
12124
|
+
return config.PDF_MAX_BYTES + 1024 * 1024;
|
|
12125
|
+
}
|
|
12126
|
+
function tooLarge(bytes) {
|
|
12127
|
+
return new PayloadTooLargeError(
|
|
12128
|
+
`PDF too large (${(bytes / (1024 * 1024)).toFixed(1)} MB, max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12129
|
+
);
|
|
12130
|
+
}
|
|
12131
|
+
function field(body, name) {
|
|
12132
|
+
const v = body[name];
|
|
12133
|
+
const one = Array.isArray(v) ? v[0] : v;
|
|
12134
|
+
return typeof one === "string" ? one.trim() : "";
|
|
12135
|
+
}
|
|
12136
|
+
function tagsField(body) {
|
|
12137
|
+
const v = body["tags"];
|
|
12138
|
+
if (v === void 0) return void 0;
|
|
12139
|
+
const raw = (Array.isArray(v) ? v : [v]).filter((x) => typeof x === "string");
|
|
12140
|
+
const tags = raw.flatMap((s) => s.split(",")).map((t) => t.trim().replace(/^#/, "")).filter(Boolean);
|
|
12141
|
+
return tags;
|
|
12142
|
+
}
|
|
12143
|
+
function titleFromFilename(name) {
|
|
12144
|
+
return name.split(/[/\\]/).pop().replace(/\.pdf$/i, "").replace(/[_]+/g, " ").trim().slice(0, 200);
|
|
12145
|
+
}
|
|
12146
|
+
pdfNodeRoutes.post(
|
|
12147
|
+
"/pdf",
|
|
12148
|
+
(c, next) => bodyLimit({
|
|
12149
|
+
maxSize: pdfMaxRequestBytes(),
|
|
12150
|
+
onError: (c2) => c2.json(
|
|
12151
|
+
{
|
|
12152
|
+
error: `PDF too large (max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12153
|
+
},
|
|
12154
|
+
413
|
|
12155
|
+
)
|
|
12156
|
+
})(c, next),
|
|
12157
|
+
async (c) => {
|
|
12158
|
+
const nestId = c.req.param("nestId");
|
|
12159
|
+
const lenStr = c.req.header("Content-Length");
|
|
12160
|
+
if (lenStr) {
|
|
12161
|
+
const len = parseInt(lenStr, 10);
|
|
12162
|
+
if (Number.isFinite(len) && len > pdfMaxRequestBytes()) throw tooLarge(len);
|
|
12163
|
+
}
|
|
12164
|
+
const body = await c.req.parseBody({ all: true });
|
|
12165
|
+
const fileField = Array.isArray(body["file"]) ? body["file"][0] : body["file"];
|
|
12166
|
+
if (!(fileField instanceof File)) {
|
|
12167
|
+
throw new ValidationError('multipart field "file" (a PDF) is required');
|
|
12168
|
+
}
|
|
12169
|
+
const file = fileField;
|
|
12170
|
+
if (file.size > config.PDF_MAX_BYTES) throw tooLarge(file.size);
|
|
12171
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
12172
|
+
if (!isPdf(bytes)) {
|
|
12173
|
+
throw new ValidationError("That file is not a PDF (it does not start with %PDF-).");
|
|
12174
|
+
}
|
|
12175
|
+
const userEmail = await resolveCallerEmail(c.get("userId"));
|
|
12176
|
+
const title = field(body, "title");
|
|
12177
|
+
const tags = tagsField(body);
|
|
12178
|
+
const client = attributeRequest(c, void 0);
|
|
12179
|
+
const pdf = { bytes, filename: file.name || void 0 };
|
|
12180
|
+
const targetId = field(body, "nodeId");
|
|
12181
|
+
if (targetId) {
|
|
12182
|
+
assertSafeNodeId(targetId);
|
|
12183
|
+
const changeNote = field(body, "changeNote") || "Replaced the PDF";
|
|
12184
|
+
const { node: node2, version: version2 } = await updateNode(
|
|
12185
|
+
nestId,
|
|
12186
|
+
targetId,
|
|
12187
|
+
{
|
|
12188
|
+
pdf,
|
|
12189
|
+
...title ? { title } : {},
|
|
12190
|
+
...tags ? { tags } : {},
|
|
12191
|
+
changeNote,
|
|
12192
|
+
client
|
|
12193
|
+
},
|
|
12194
|
+
userEmail
|
|
12195
|
+
);
|
|
12196
|
+
return c.json({ node: toNodeResponse(node2), version: version2 });
|
|
12197
|
+
}
|
|
12198
|
+
const folder = field(body, "folder");
|
|
12199
|
+
const { node, version } = await createNode(
|
|
12200
|
+
nestId,
|
|
12201
|
+
{
|
|
12202
|
+
title: title || titleFromFilename(file.name || "") || "Untitled PDF",
|
|
12203
|
+
content: "",
|
|
12204
|
+
type: PDF_NODE_TYPE,
|
|
12205
|
+
...tags ? { tags } : {},
|
|
12206
|
+
...folder ? { folder } : {},
|
|
12207
|
+
client,
|
|
12208
|
+
pdf
|
|
12209
|
+
},
|
|
12210
|
+
userEmail
|
|
12211
|
+
);
|
|
12212
|
+
const resolved = await resolveStewardsForNode(nestId, node.id);
|
|
12213
|
+
return c.json(
|
|
12214
|
+
{
|
|
12215
|
+
node: toNodeResponse(node),
|
|
12216
|
+
version,
|
|
12217
|
+
stewards: resolved.length > 0 ? resolved.map((r) => ({
|
|
12218
|
+
email: r.steward.userEmail,
|
|
12219
|
+
role: r.steward.role,
|
|
12220
|
+
source: r.source
|
|
12221
|
+
})) : void 0
|
|
12222
|
+
},
|
|
12223
|
+
201
|
|
12224
|
+
);
|
|
12225
|
+
}
|
|
12226
|
+
);
|
|
12227
|
+
pdfNodeRoutes.get("/:nodeId{.+}/pdf", async (c, next) => {
|
|
12228
|
+
const nestId = c.req.param("nestId");
|
|
12229
|
+
const nodeId = c.req.param("nodeId");
|
|
12230
|
+
const userId = c.get("userId");
|
|
12231
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
12232
|
+
let node;
|
|
12233
|
+
try {
|
|
12234
|
+
node = await getNodeForCaller(nestId, { id: nodeId }, userId, userEmail);
|
|
12235
|
+
} catch {
|
|
12236
|
+
return next();
|
|
12237
|
+
}
|
|
12238
|
+
if (node.frontmatter.type !== PDF_NODE_TYPE) return next();
|
|
12239
|
+
const requested = c.req.query("version");
|
|
12240
|
+
if (requested !== void 0 && !/^[1-9]\d*$/.test(requested)) {
|
|
12241
|
+
throw new ValidationError("version must be a positive integer");
|
|
12242
|
+
}
|
|
12243
|
+
const approvedOnly = c.req.query("approved_only") === "1" || c.req.query("approved_only") === "true";
|
|
12244
|
+
let version;
|
|
12245
|
+
if (approvedOnly || await isPublicReader(nestId, userId)) {
|
|
12246
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
12247
|
+
if (approved == null) {
|
|
12248
|
+
throw new NotFoundError("This document has no approved version yet.");
|
|
12249
|
+
}
|
|
12250
|
+
version = approved;
|
|
12251
|
+
} else if (requested !== void 0) {
|
|
12252
|
+
version = Number(requested);
|
|
12253
|
+
}
|
|
12254
|
+
const { storage } = await engineCache.get(nestId);
|
|
12255
|
+
let bytes;
|
|
12256
|
+
try {
|
|
12257
|
+
bytes = await readPdfBinary3(
|
|
12258
|
+
storage,
|
|
12259
|
+
nodeId,
|
|
12260
|
+
version !== void 0 ? { version } : {}
|
|
12261
|
+
);
|
|
12262
|
+
} catch (err) {
|
|
12263
|
+
const code = err?.code;
|
|
12264
|
+
if (code === "INTEGRITY_ERROR") {
|
|
12265
|
+
console.error("[pdf] integrity failure serving", nestId, nodeId, version, err);
|
|
12266
|
+
throw new AppError(
|
|
12267
|
+
500,
|
|
12268
|
+
"This PDF no longer matches the version history's record of it, so it is not being served. Ask an admin to verify the nest."
|
|
12269
|
+
);
|
|
12270
|
+
}
|
|
12271
|
+
throw new NotFoundError(
|
|
12272
|
+
version !== void 0 ? `Version ${version} of this PDF was not found.` : "This document's PDF was not found."
|
|
12273
|
+
);
|
|
12274
|
+
}
|
|
12275
|
+
const etag = `"sha256:${createHash3("sha256").update(bytes).digest("hex")}"`;
|
|
12276
|
+
const leaf = (nodeId.split("/").pop() || "document").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
12277
|
+
const disposition = c.req.query("download") === "1" ? "attachment" : "inline";
|
|
12278
|
+
const headers = {
|
|
12279
|
+
"Content-Type": "application/pdf",
|
|
12280
|
+
"Content-Disposition": `${disposition}; filename="${leaf}.pdf"`,
|
|
12281
|
+
"Content-Security-Policy": "sandbox",
|
|
12282
|
+
"X-Content-Type-Options": "nosniff",
|
|
12283
|
+
"Cache-Control": "private, no-cache",
|
|
12284
|
+
ETag: etag
|
|
12285
|
+
};
|
|
12286
|
+
const ifNoneMatch = c.req.header("If-None-Match");
|
|
12287
|
+
if (ifNoneMatch && ifNoneMatch.split(",").map((t) => t.trim()).includes(etag)) {
|
|
12288
|
+
return c.body(null, 304, headers);
|
|
12289
|
+
}
|
|
12290
|
+
return c.body(new Uint8Array(bytes), 200, headers);
|
|
12291
|
+
});
|
|
12292
|
+
|
|
12293
|
+
// src/auth/community-sites-routes.ts
|
|
12294
|
+
import { Hono as Hono23 } from "hono";
|
|
11754
12295
|
var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
11755
12296
|
var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
|
|
11756
12297
|
var serializeSite = (site) => ({
|
|
@@ -11762,7 +12303,7 @@ var serializeSite = (site) => ({
|
|
|
11762
12303
|
created_at: site.created_at,
|
|
11763
12304
|
updated_at: site.updated_at
|
|
11764
12305
|
});
|
|
11765
|
-
var communitySiteRoutes = new
|
|
12306
|
+
var communitySiteRoutes = new Hono23();
|
|
11766
12307
|
communitySiteRoutes.get("/", async (c) => {
|
|
11767
12308
|
if (!await serverAdminAllowed(c))
|
|
11768
12309
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
@@ -11850,7 +12391,7 @@ communitySiteRoutes.delete("/:id", async (c) => {
|
|
|
11850
12391
|
});
|
|
11851
12392
|
|
|
11852
12393
|
// src/mcp/server-routes.ts
|
|
11853
|
-
import { Hono as
|
|
12394
|
+
import { Hono as Hono24 } from "hono";
|
|
11854
12395
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11855
12396
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
11856
12397
|
import { z as z2 } from "zod";
|
|
@@ -11973,116 +12514,39 @@ var NEST_ARG_OPTIONAL = {
|
|
|
11973
12514
|
description: "Target nest \u2014 id or name. Omit to search/list across ALL accessible nests."
|
|
11974
12515
|
}
|
|
11975
12516
|
};
|
|
12517
|
+
var SERVER_NEST_TOOLS = [
|
|
12518
|
+
// Fan-out reads — `nest` optional: omit it to span every accessible nest.
|
|
12519
|
+
...FANOUT_READ_TOOLS,
|
|
12520
|
+
// Targeted reads and writes — `nest` required.
|
|
12521
|
+
"context_get",
|
|
12522
|
+
"context_resolve",
|
|
12523
|
+
"context_comments",
|
|
12524
|
+
"context_versions",
|
|
12525
|
+
"context_create",
|
|
12526
|
+
"context_update",
|
|
12527
|
+
"context_delete",
|
|
12528
|
+
"context_move",
|
|
12529
|
+
"context_submit_review"
|
|
12530
|
+
];
|
|
12531
|
+
function withNestArg(tool) {
|
|
12532
|
+
const fanout = FANOUT_READ_TOOLS.has(tool.name);
|
|
12533
|
+
const schema = tool.inputSchema;
|
|
12534
|
+
return {
|
|
12535
|
+
name: tool.name,
|
|
12536
|
+
description: fanout ? `${tool.description} On this server-level endpoint, omit \`nest\` to run it across ALL accessible nests (each hit names its nest).` : `${tool.description} On this server-level endpoint, \`nest\` (id, name or slug) is required.`,
|
|
12537
|
+
inputSchema: {
|
|
12538
|
+
...schema,
|
|
12539
|
+
properties: { ...fanout ? NEST_ARG_OPTIONAL : NEST_ARG, ...schema.properties },
|
|
12540
|
+
required: [...fanout ? [] : ["nest"], ...schema.required ?? []]
|
|
12541
|
+
}
|
|
12542
|
+
};
|
|
12543
|
+
}
|
|
11976
12544
|
var SERVER_TOOL_DEFINITIONS = [
|
|
11977
12545
|
{
|
|
11978
12546
|
name: "nest_index",
|
|
11979
12547
|
description: "Grounded index of every nest these credentials can read: name, id, permission, document count. Call this FIRST.",
|
|
11980
12548
|
inputSchema: { type: "object", properties: {} }
|
|
11981
12549
|
},
|
|
11982
|
-
// ─── Fan-out reads (nest optional) ─────────────────────────────────────
|
|
11983
|
-
{
|
|
11984
|
-
name: "context_query",
|
|
11985
|
-
description: "Run a structured selector query. Omit `nest` to query ALL accessible nests. Selector: #tag, type:X, [[Title]], scope:X, +AND, |OR, -NOT.",
|
|
11986
|
-
inputSchema: {
|
|
11987
|
-
type: "object",
|
|
11988
|
-
properties: {
|
|
11989
|
-
...NEST_ARG_OPTIONAL,
|
|
11990
|
-
query: { type: "string", description: "Selector query" },
|
|
11991
|
-
hops: {
|
|
11992
|
-
type: "number",
|
|
11993
|
-
description: "Graph traversal depth from the matches (default: 2)"
|
|
11994
|
-
}
|
|
11995
|
-
},
|
|
11996
|
-
required: ["query"]
|
|
11997
|
-
}
|
|
11998
|
-
},
|
|
11999
|
-
{
|
|
12000
|
-
name: "context_search",
|
|
12001
|
-
description: "Full-text keyword search. Omit `nest` to search ALL accessible nests. Returns count and first `limit` hits.",
|
|
12002
|
-
inputSchema: {
|
|
12003
|
-
type: "object",
|
|
12004
|
-
properties: {
|
|
12005
|
-
...NEST_ARG_OPTIONAL,
|
|
12006
|
-
query: { type: "string", description: "Search terms" },
|
|
12007
|
-
limit: {
|
|
12008
|
-
type: "number",
|
|
12009
|
-
description: "Max hits to return (default 20, max 200)"
|
|
12010
|
-
}
|
|
12011
|
-
},
|
|
12012
|
-
required: ["query"]
|
|
12013
|
-
}
|
|
12014
|
-
},
|
|
12015
|
-
{
|
|
12016
|
-
name: "context_list",
|
|
12017
|
-
description: "Browse contents with optional type, tag, or limit filters. Omit `nest` to list ALL accessible nests.",
|
|
12018
|
-
inputSchema: {
|
|
12019
|
-
type: "object",
|
|
12020
|
-
properties: {
|
|
12021
|
-
...NEST_ARG_OPTIONAL,
|
|
12022
|
-
type: { type: "string", description: "Filter by node type" },
|
|
12023
|
-
tag: { type: "string", description: "Filter by tag" },
|
|
12024
|
-
limit: { type: "number", description: "Max nodes to return" }
|
|
12025
|
-
}
|
|
12026
|
-
}
|
|
12027
|
-
},
|
|
12028
|
-
{
|
|
12029
|
-
name: "context_overview",
|
|
12030
|
-
description: "Map of nests: node count, types, tags, title+snippet per node. Omit `nest` for ALL accessible nests.",
|
|
12031
|
-
inputSchema: {
|
|
12032
|
-
type: "object",
|
|
12033
|
-
properties: { ...NEST_ARG_OPTIONAL }
|
|
12034
|
-
}
|
|
12035
|
-
},
|
|
12036
|
-
// ─── Targeted reads (nest required) ────────────────────────────────────
|
|
12037
|
-
{
|
|
12038
|
-
name: "context_get",
|
|
12039
|
-
description: "Get the FULL content of a node in a nest by title or id.",
|
|
12040
|
-
inputSchema: {
|
|
12041
|
-
type: "object",
|
|
12042
|
-
properties: {
|
|
12043
|
-
...NEST_ARG,
|
|
12044
|
-
title: { type: "string", description: "Title of the node" },
|
|
12045
|
-
id: { type: "string", description: "ID of the node" }
|
|
12046
|
-
},
|
|
12047
|
-
required: ["nest"]
|
|
12048
|
-
}
|
|
12049
|
-
},
|
|
12050
|
-
{
|
|
12051
|
-
name: "context_resolve",
|
|
12052
|
-
description: "Full deterministic context resolution against one nest \u2014 selector + hops + token budget, complete node bodies.",
|
|
12053
|
-
inputSchema: {
|
|
12054
|
-
type: "object",
|
|
12055
|
-
properties: {
|
|
12056
|
-
...NEST_ARG,
|
|
12057
|
-
selector: { type: "string", description: "Selector query string" },
|
|
12058
|
-
max_tokens: {
|
|
12059
|
-
type: "number",
|
|
12060
|
-
description: "Approximate token budget (default: 8000)"
|
|
12061
|
-
},
|
|
12062
|
-
hops: {
|
|
12063
|
-
type: "number",
|
|
12064
|
-
description: "Graph traversal depth from the matches (default: 2)"
|
|
12065
|
-
}
|
|
12066
|
-
},
|
|
12067
|
-
required: ["nest", "selector"]
|
|
12068
|
-
}
|
|
12069
|
-
},
|
|
12070
|
-
{
|
|
12071
|
-
// Read-only, so it belongs on this endpoint: an agent reading a nest's
|
|
12072
|
-
// documents across nests also needs the feedback left on them. Writing a
|
|
12073
|
-
// comment stays on the per-nest endpoint with the other write tools.
|
|
12074
|
-
name: "context_comments",
|
|
12075
|
-
description: "Get the review comments/annotations on a document in a nest \u2014 anchored quotes, threaded replies, and open/resolved status. Use to pull human feedback into context before acting on a document.",
|
|
12076
|
-
inputSchema: {
|
|
12077
|
-
type: "object",
|
|
12078
|
-
properties: {
|
|
12079
|
-
nest: { type: "string", description: "Nest name or id (see nest_index)" },
|
|
12080
|
-
title: { type: "string", description: "Title of the node" },
|
|
12081
|
-
id: { type: "string", description: "ID of the node" }
|
|
12082
|
-
},
|
|
12083
|
-
required: ["nest"]
|
|
12084
|
-
}
|
|
12085
|
-
},
|
|
12086
12550
|
{
|
|
12087
12551
|
name: "nest_create",
|
|
12088
12552
|
description: "Create a new nest owned by the caller. Returns the new nest's id \u2014 target it with the other tools afterwards.",
|
|
@@ -12095,93 +12559,18 @@ var SERVER_TOOL_DEFINITIONS = [
|
|
|
12095
12559
|
required: ["name"]
|
|
12096
12560
|
}
|
|
12097
12561
|
},
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
name
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
type: "object",
|
|
12104
|
-
properties: {
|
|
12105
|
-
...NEST_ARG,
|
|
12106
|
-
title: { type: "string", description: "Descriptive title" },
|
|
12107
|
-
content: { type: "string", description: "Markdown content body" },
|
|
12108
|
-
type: { type: "string", description: "Node type (default: document)" },
|
|
12109
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags" },
|
|
12110
|
-
scope: { type: "string", description: "Visibility scope" },
|
|
12111
|
-
folder: {
|
|
12112
|
-
type: "string",
|
|
12113
|
-
description: 'Folder path under nodes/ (e.g. "gtm/deals"); segments are slugified'
|
|
12114
|
-
}
|
|
12115
|
-
},
|
|
12116
|
-
required: ["nest", "title", "content"]
|
|
12117
|
-
}
|
|
12118
|
-
},
|
|
12119
|
-
{
|
|
12120
|
-
name: "context_update",
|
|
12121
|
-
description: "Update an existing node by title or id. Needs write permission. Use `content` to replace, `append` to add.",
|
|
12122
|
-
inputSchema: {
|
|
12123
|
-
type: "object",
|
|
12124
|
-
properties: {
|
|
12125
|
-
...NEST_ARG,
|
|
12126
|
-
title: { type: "string", description: "Title of node to update" },
|
|
12127
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12128
|
-
content: { type: "string", description: "New content (replaces)" },
|
|
12129
|
-
append: { type: "string", description: "Content to append" },
|
|
12130
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags to add" },
|
|
12131
|
-
status: { type: "string", description: "New status: draft|pending_review|approved|published" },
|
|
12132
|
-
changeNote: { type: "string", description: "Why this changed \u2014 recorded in history" }
|
|
12133
|
-
},
|
|
12134
|
-
required: ["nest"]
|
|
12135
|
-
}
|
|
12136
|
-
},
|
|
12137
|
-
{
|
|
12138
|
-
name: "context_delete",
|
|
12139
|
-
description: "Delete a node by title or id. Needs write permission. Irreversible.",
|
|
12140
|
-
inputSchema: {
|
|
12141
|
-
type: "object",
|
|
12142
|
-
properties: {
|
|
12143
|
-
...NEST_ARG,
|
|
12144
|
-
title: { type: "string", description: "Title of node to delete" },
|
|
12145
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" }
|
|
12146
|
-
},
|
|
12147
|
-
required: ["nest"]
|
|
12148
|
-
}
|
|
12149
|
-
},
|
|
12150
|
-
{
|
|
12151
|
-
name: "context_move",
|
|
12152
|
-
description: "Move a node to a different folder. The id changes; history and links follow. Needs write permission.",
|
|
12153
|
-
inputSchema: {
|
|
12154
|
-
type: "object",
|
|
12155
|
-
properties: {
|
|
12156
|
-
...NEST_ARG,
|
|
12157
|
-
title: { type: "string", description: "Title of node to move" },
|
|
12158
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12159
|
-
folder: { type: "string", description: 'Destination folder (e.g. "gtm/deals"); "" for root' }
|
|
12160
|
-
},
|
|
12161
|
-
required: ["nest", "folder"]
|
|
12162
|
-
}
|
|
12163
|
-
},
|
|
12164
|
-
{
|
|
12165
|
-
name: "context_submit_review",
|
|
12166
|
-
description: "Submit a node for steward review. Stewards will be notified. Use on governed nests.",
|
|
12167
|
-
inputSchema: {
|
|
12168
|
-
type: "object",
|
|
12169
|
-
properties: {
|
|
12170
|
-
...NEST_ARG,
|
|
12171
|
-
title: { type: "string", description: "Title of node to submit" },
|
|
12172
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12173
|
-
note: { type: "string", description: "Note for the reviewer" },
|
|
12174
|
-
priority: { type: "string", description: "Priority: low, normal, high, urgent" }
|
|
12175
|
-
},
|
|
12176
|
-
required: ["nest"]
|
|
12177
|
-
}
|
|
12178
|
-
}
|
|
12562
|
+
...SERVER_NEST_TOOLS.map((name) => {
|
|
12563
|
+
const tool = TOOL_DEFINITIONS.find((t) => t.name === name);
|
|
12564
|
+
if (!tool) throw new Error(`server-level tool ${name} has no per-nest definition`);
|
|
12565
|
+
return withNestArg(tool);
|
|
12566
|
+
})
|
|
12179
12567
|
];
|
|
12180
|
-
async function handleServerToolCall(toolName, args, userId, userEmail, nestScope, baseUrl) {
|
|
12568
|
+
async function handleServerToolCall(toolName, args, userId, userEmail, nestScope, baseUrl, client) {
|
|
12181
12569
|
if (toolName === "nest_index") {
|
|
12182
12570
|
const nests = await buildNestIndex(userId, nestScope);
|
|
12183
|
-
|
|
12184
|
-
|
|
12571
|
+
const data = { nests };
|
|
12572
|
+
if (!nests.length) return { text: "No nests accessible with these credentials.", data };
|
|
12573
|
+
return { text: renderNestIndexMarkdown(nests, baseUrl), data };
|
|
12185
12574
|
}
|
|
12186
12575
|
if (SERVER_WRITE_TOOLS.has(toolName)) {
|
|
12187
12576
|
if (isSuspended()) {
|
|
@@ -12213,9 +12602,6 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
12213
12602
|
const nests = await buildNestIndex(userId, nestScope);
|
|
12214
12603
|
if (!nests.length) return "No nests accessible with these credentials.";
|
|
12215
12604
|
const nonEmpty = nests.filter((n) => (n.document_count ?? 0) > 0);
|
|
12216
|
-
if (!nonEmpty.length) {
|
|
12217
|
-
return "All accessible nests are empty. Use context_create to add documents first.";
|
|
12218
|
-
}
|
|
12219
12605
|
const results = await Promise.allSettled(
|
|
12220
12606
|
nonEmpty.map(async (n) => {
|
|
12221
12607
|
const engine2 = await engineCache.get(n.id);
|
|
@@ -12226,25 +12612,66 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
12226
12612
|
nestId: n.id,
|
|
12227
12613
|
userId,
|
|
12228
12614
|
userEmail,
|
|
12229
|
-
baseUrl
|
|
12615
|
+
baseUrl,
|
|
12616
|
+
// Same attribution the single-nest branch below passes (§9.4). A
|
|
12617
|
+
// fan-out read is still one call per nest on the trace, and each row
|
|
12618
|
+
// has to name the caller — without this the derived agent/session is
|
|
12619
|
+
// dropped and only an explicit `client` in the args survives.
|
|
12620
|
+
client
|
|
12230
12621
|
});
|
|
12231
12622
|
return { nest: n, result };
|
|
12232
12623
|
})
|
|
12233
12624
|
);
|
|
12234
12625
|
const parts = [];
|
|
12626
|
+
let firstError;
|
|
12627
|
+
let firstRejection;
|
|
12628
|
+
const tag = (n, items) => Array.isArray(items) ? items.map((it) => ({ ...it, nest: { id: n.id, name: n.name } })) : [];
|
|
12629
|
+
const perNestDocs = [];
|
|
12630
|
+
const perNestHits = [];
|
|
12631
|
+
let count = 0;
|
|
12632
|
+
let nodesTraversed = 0;
|
|
12633
|
+
let hopsUsed = 0;
|
|
12634
|
+
let mode;
|
|
12235
12635
|
for (const r of results) {
|
|
12236
|
-
if (r.status
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
parts.push(`## ${n.name} (${n.id})
|
|
12240
|
-
|
|
12241
|
-
${text}`);
|
|
12636
|
+
if (r.status !== "fulfilled") {
|
|
12637
|
+
firstRejection ??= r;
|
|
12638
|
+
continue;
|
|
12242
12639
|
}
|
|
12640
|
+
const { nest: n, result } = r.value;
|
|
12641
|
+
if (typeof result !== "string" && result.isError) {
|
|
12642
|
+
firstError ??= result;
|
|
12643
|
+
continue;
|
|
12644
|
+
}
|
|
12645
|
+
parts.push(`## ${n.name} (${n.id})
|
|
12646
|
+
|
|
12647
|
+
${typeof result === "string" ? result : result.text}`);
|
|
12648
|
+
const d = typeof result === "string" ? void 0 : result.data;
|
|
12649
|
+
if (!d) continue;
|
|
12650
|
+
perNestDocs.push(tag(n, d.documents));
|
|
12651
|
+
perNestHits.push(tag(n, d.results));
|
|
12652
|
+
count += d.count ?? 0;
|
|
12653
|
+
nodesTraversed += d.traversal?.nodes_traversed ?? 0;
|
|
12654
|
+
hopsUsed = Math.max(hopsUsed, d.traversal?.hops_used ?? 0);
|
|
12655
|
+
mode ??= d.traversal?.mode;
|
|
12243
12656
|
}
|
|
12244
|
-
|
|
12245
|
-
|
|
12657
|
+
const interleave = (lists) => {
|
|
12658
|
+
const out = [];
|
|
12659
|
+
for (let i = 0; lists.some((l) => i < l.length); i++) {
|
|
12660
|
+
for (const l of lists) if (i < l.length) out.push(l[i]);
|
|
12661
|
+
}
|
|
12662
|
+
return out;
|
|
12663
|
+
};
|
|
12664
|
+
if (!parts.length && firstError) return firstError;
|
|
12665
|
+
if (!parts.length && firstRejection) throw firstRejection.reason;
|
|
12666
|
+
const documents = interleave(perNestDocs);
|
|
12667
|
+
const listLimit = typeof args.limit === "number" && args.limit >= 1 ? Math.floor(args.limit) : 50;
|
|
12668
|
+
const data = toolName === "context_search" ? { results: interleave(perNestHits).slice(0, normalizeSearchLimit(args.limit)), count } : toolName === "context_query" ? { documents, traversal: { mode: mode ?? "selector", hops_used: hopsUsed, nodes_traversed: nodesTraversed } } : { documents: documents.slice(0, listLimit) };
|
|
12669
|
+
const reply = (text) => toolName === "context_overview" ? text : { text, data };
|
|
12670
|
+
if (!nonEmpty.length) {
|
|
12671
|
+
return reply("All accessible nests are empty. Use context_create to add documents first.");
|
|
12246
12672
|
}
|
|
12247
|
-
|
|
12673
|
+
if (!parts.length) return reply("No results found across any accessible nests.");
|
|
12674
|
+
return reply(parts.join("\n\n---\n\n"));
|
|
12248
12675
|
}
|
|
12249
12676
|
const inaccessible = `Nest not found or not accessible: "${nestArg}". Use nest_index to see what these credentials can read.`;
|
|
12250
12677
|
if (!nestArg) {
|
|
@@ -12262,10 +12689,11 @@ ${text}`);
|
|
|
12262
12689
|
nestId: nest.id,
|
|
12263
12690
|
userId,
|
|
12264
12691
|
userEmail,
|
|
12265
|
-
baseUrl
|
|
12692
|
+
baseUrl,
|
|
12693
|
+
client
|
|
12266
12694
|
});
|
|
12267
12695
|
}
|
|
12268
|
-
function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
12696
|
+
function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
|
|
12269
12697
|
const server = new McpServer2(
|
|
12270
12698
|
{ name: "contextnest-server", version: "1.0.0" },
|
|
12271
12699
|
{ capabilities: { tools: {} } }
|
|
@@ -12275,14 +12703,15 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
12275
12703
|
const required = tool.inputSchema.required || [];
|
|
12276
12704
|
const shape = {};
|
|
12277
12705
|
for (const [key, def] of Object.entries(props)) {
|
|
12278
|
-
let
|
|
12279
|
-
if (def.type === "string")
|
|
12280
|
-
else if (def.type === "number")
|
|
12281
|
-
else if (def.type === "array")
|
|
12282
|
-
else
|
|
12283
|
-
if (!required.includes(key))
|
|
12284
|
-
shape[key] =
|
|
12285
|
-
}
|
|
12706
|
+
let field2;
|
|
12707
|
+
if (def.type === "string") field2 = z2.string();
|
|
12708
|
+
else if (def.type === "number") field2 = z2.number();
|
|
12709
|
+
else if (def.type === "array") field2 = z2.array(z2.string());
|
|
12710
|
+
else field2 = z2.any();
|
|
12711
|
+
if (!required.includes(key)) field2 = field2.optional();
|
|
12712
|
+
shape[key] = field2;
|
|
12713
|
+
}
|
|
12714
|
+
Object.assign(shape, clientToolField);
|
|
12286
12715
|
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
12287
12716
|
const out = await handleServerToolCall(
|
|
12288
12717
|
tool.name,
|
|
@@ -12290,14 +12719,15 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
12290
12719
|
userId,
|
|
12291
12720
|
userEmail,
|
|
12292
12721
|
nestScope,
|
|
12293
|
-
baseUrl
|
|
12722
|
+
baseUrl,
|
|
12723
|
+
attributeRequest(c, void 0, connectionClient(server))
|
|
12294
12724
|
);
|
|
12295
12725
|
return toCallToolResult(out);
|
|
12296
12726
|
});
|
|
12297
12727
|
}
|
|
12298
12728
|
return server;
|
|
12299
12729
|
}
|
|
12300
|
-
var serverMcpRoutes = new
|
|
12730
|
+
var serverMcpRoutes = new Hono24();
|
|
12301
12731
|
serverMcpRoutes.all("/", async (c) => {
|
|
12302
12732
|
const userId = c.get("userId");
|
|
12303
12733
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -12305,7 +12735,8 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
12305
12735
|
userId,
|
|
12306
12736
|
userEmail,
|
|
12307
12737
|
c.get("nestScope"),
|
|
12308
|
-
requestBaseUrl(c.req.url)
|
|
12738
|
+
requestBaseUrl(c.req.url),
|
|
12739
|
+
c
|
|
12309
12740
|
);
|
|
12310
12741
|
const transport = new WebStandardStreamableHTTPServerTransport2({
|
|
12311
12742
|
sessionIdGenerator: void 0,
|
|
@@ -12319,7 +12750,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
12319
12750
|
await server.server.close();
|
|
12320
12751
|
}
|
|
12321
12752
|
});
|
|
12322
|
-
var nestIndexRoutes = new
|
|
12753
|
+
var nestIndexRoutes = new Hono24();
|
|
12323
12754
|
nestIndexRoutes.get("/", async (c) => {
|
|
12324
12755
|
const userId = c.get("userId");
|
|
12325
12756
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -12413,6 +12844,20 @@ Write endpoints (need write permission):
|
|
|
12413
12844
|
- \`GET ${B}/me/drafts\` \u2014 your documents never submitted for review
|
|
12414
12845
|
- \`GET ${B}/me/notifications\` \u2014 watcher and review notifications
|
|
12415
12846
|
|
|
12847
|
+
## Say who you are: \`client\`
|
|
12848
|
+
|
|
12849
|
+
Every write body and every MCP tool call takes an optional
|
|
12850
|
+
\`client\` object \u2014 \`{ "agent": "claude-code", "session_id": "s-9f2" }\`, plus any
|
|
12851
|
+
custom scalar keys. A write that seals a version records it on that version
|
|
12852
|
+
(\`GET /nodes/<id>/versions\` and \`context_versions\` serve it back); every call
|
|
12853
|
+
records it on this server's activity trace, which is how a READ gets attributed.
|
|
12854
|
+
|
|
12855
|
+
Send it. Nothing requires it and no call is refused for omitting it, but a vault
|
|
12856
|
+
serving several agents cannot otherwise tell which of you wrote what. It is a
|
|
12857
|
+
label, never an identity claim: it is not authenticated, grants nothing, and is
|
|
12858
|
+
never part of a hash. Bounded \u2014 scalar values up to 512 chars, at most 16 custom
|
|
12859
|
+
keys beyond \`agent\` and \`session_id\`.
|
|
12860
|
+
|
|
12416
12861
|
## MCP (Model Context Protocol)
|
|
12417
12862
|
|
|
12418
12863
|
Streamable HTTP, stateless. Two endpoints:
|
|
@@ -12421,7 +12866,7 @@ Streamable HTTP, stateless. Two endpoints:
|
|
|
12421
12866
|
Tools: \`nest_index\` (call first), then:
|
|
12422
12867
|
- Fan-out reads (omit \`nest\` to search/list ALL nests): \`context_search\`,
|
|
12423
12868
|
\`context_list\`, \`context_query\`, \`context_overview\`
|
|
12424
|
-
- Targeted reads (\`nest\` required): \`context_get\`, \`context_resolve\`, \`context_comments\`
|
|
12869
|
+
- Targeted reads (\`nest\` required): \`context_get\`, \`context_resolve\`, \`context_comments\`, \`context_versions\`
|
|
12425
12870
|
- Writes (\`nest\` required): \`nest_create\`, \`context_create\`, \`context_update\`,
|
|
12426
12871
|
\`context_delete\`, \`context_move\`, \`context_submit_review\`
|
|
12427
12872
|
- \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, the full toolset. Same names as the
|
|
@@ -12458,9 +12903,11 @@ on the command line.
|
|
|
12458
12903
|
export CONTEXTNEST_API_KEY=<api-key>
|
|
12459
12904
|
# every nest these credentials can read, through the server-level endpoint
|
|
12460
12905
|
ctx vault add contextnest --url ${B}/mcp --bearer-env CONTEXTNEST_API_KEY
|
|
12461
|
-
#
|
|
12906
|
+
ctx vault list # lists contextnest/<nest> for each nest
|
|
12907
|
+
ctx search "pricing" --vault contextnest # reads span every nest
|
|
12908
|
+
ctx update nodes/x --tags a,b --vault contextnest/<nest> # a write names one nest
|
|
12909
|
+
# or pin one nest by its own endpoint
|
|
12462
12910
|
ctx vault add mynest --url ${B}/nests/<nest-id>/mcp --bearer-env CONTEXTNEST_API_KEY
|
|
12463
|
-
ctx query "#gtm" --vault mynest
|
|
12464
12911
|
\`\`\`
|
|
12465
12912
|
|
|
12466
12913
|
Servers in "open" auth mode: drop \`--bearer-env\`. Pushing a local folder up
|
|
@@ -12479,14 +12926,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
|
12479
12926
|
through steward review before they become AI-visible.
|
|
12480
12927
|
`;
|
|
12481
12928
|
}
|
|
12482
|
-
var llmsTxtRoutes = new
|
|
12929
|
+
var llmsTxtRoutes = new Hono24();
|
|
12483
12930
|
llmsTxtRoutes.get("/", (c) => {
|
|
12484
12931
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
12485
12932
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
12486
12933
|
});
|
|
12487
12934
|
|
|
12488
12935
|
// src/governance/routes.ts
|
|
12489
|
-
import { Hono as
|
|
12936
|
+
import { Hono as Hono25 } from "hono";
|
|
12490
12937
|
|
|
12491
12938
|
// src/governance/feed-service.ts
|
|
12492
12939
|
var SNIPPET_CHARS = 200;
|
|
@@ -12918,7 +13365,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
12918
13365
|
if (Number.isNaN(n) || n < 1) return def;
|
|
12919
13366
|
return Math.min(n, max);
|
|
12920
13367
|
}
|
|
12921
|
-
var governanceRoutes = new
|
|
13368
|
+
var governanceRoutes = new Hono25();
|
|
12922
13369
|
governanceRoutes.get("/stewards", async (c) => {
|
|
12923
13370
|
const nestId = c.req.param("nestId");
|
|
12924
13371
|
const scope = c.req.query("scope");
|
|
@@ -13058,6 +13505,7 @@ governanceRoutes.post("/review-queue/bulk", async (c) => {
|
|
|
13058
13505
|
}
|
|
13059
13506
|
const userEmail = await getUserEmail4(c);
|
|
13060
13507
|
const baseUrl = requestBaseUrl(c.req.url);
|
|
13508
|
+
const client = attributeRequest(c, body.client);
|
|
13061
13509
|
const results = [];
|
|
13062
13510
|
for (const nodeId of nodeIds) {
|
|
13063
13511
|
try {
|
|
@@ -13068,7 +13516,8 @@ governanceRoutes.post("/review-queue/bulk", async (c) => {
|
|
|
13068
13516
|
approvedBy: userEmail,
|
|
13069
13517
|
note: body.note,
|
|
13070
13518
|
override: false,
|
|
13071
|
-
baseUrl
|
|
13519
|
+
baseUrl,
|
|
13520
|
+
client
|
|
13072
13521
|
});
|
|
13073
13522
|
} else {
|
|
13074
13523
|
await reject({
|
|
@@ -13286,7 +13735,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
13286
13735
|
const activity = await getActivity({ nestId, limit });
|
|
13287
13736
|
return c.json({ activity });
|
|
13288
13737
|
});
|
|
13289
|
-
var governanceNodeRoutes = new
|
|
13738
|
+
var governanceNodeRoutes = new Hono25();
|
|
13290
13739
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
13291
13740
|
const nestId = c.req.param("nestId");
|
|
13292
13741
|
const nodeId = c.req.param("nodeId");
|
|
@@ -13492,7 +13941,10 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
|
|
|
13492
13941
|
requestedBy: userEmail,
|
|
13493
13942
|
note: body.note,
|
|
13494
13943
|
priority: body.priority,
|
|
13495
|
-
baseUrl: requestBaseUrl(c.req.url)
|
|
13944
|
+
baseUrl: requestBaseUrl(c.req.url),
|
|
13945
|
+
// Submitting seals the draft into the engine's append-only chain, so the
|
|
13946
|
+
// block lands on that entry as well as on this request's trace row.
|
|
13947
|
+
client: attributeRequest(c, body.client)
|
|
13496
13948
|
});
|
|
13497
13949
|
} catch (err) {
|
|
13498
13950
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -13626,7 +14078,8 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
|
|
|
13626
14078
|
approvedBy: userEmail,
|
|
13627
14079
|
note: body.note,
|
|
13628
14080
|
override: body.override && isAdmin,
|
|
13629
|
-
baseUrl: requestBaseUrl(c.req.url)
|
|
14081
|
+
baseUrl: requestBaseUrl(c.req.url),
|
|
14082
|
+
client: attributeRequest(c, body.client)
|
|
13630
14083
|
});
|
|
13631
14084
|
return c.json({ review: request });
|
|
13632
14085
|
} catch (err) {
|
|
@@ -13641,6 +14094,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
|
|
|
13641
14094
|
throw new ValidationError("Rejection note is required");
|
|
13642
14095
|
}
|
|
13643
14096
|
const userEmail = await getUserEmail4(c);
|
|
14097
|
+
attributeRequest(c, body.client);
|
|
13644
14098
|
try {
|
|
13645
14099
|
const request = await reject({
|
|
13646
14100
|
nestId,
|
|
@@ -13753,6 +14207,26 @@ function posthogKeyError(value) {
|
|
|
13753
14207
|
return null;
|
|
13754
14208
|
}
|
|
13755
14209
|
|
|
14210
|
+
// src/shared/public-base-url.ts
|
|
14211
|
+
var UNSAFE = /["'`\\$;|&<>(){}\s]/;
|
|
14212
|
+
function publicBaseUrlError(value) {
|
|
14213
|
+
const v = value.trim();
|
|
14214
|
+
if (!v) return null;
|
|
14215
|
+
let parsed;
|
|
14216
|
+
try {
|
|
14217
|
+
parsed = new URL(v);
|
|
14218
|
+
} catch {
|
|
14219
|
+
return "Public base URL must be a full http(s) URL, e.g. https://nest.example.com";
|
|
14220
|
+
}
|
|
14221
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
14222
|
+
return "Public base URL must use http or https, e.g. https://nest.example.com";
|
|
14223
|
+
if (!parsed.hostname)
|
|
14224
|
+
return "Public base URL must include a hostname, e.g. https://nest.example.com";
|
|
14225
|
+
if (UNSAFE.test(v))
|
|
14226
|
+
return "Public base URL must not contain spaces, quotes or shell characters (\" ' ` \\ $ ; | & < > ( ) { }) \u2014 it is pasted into terminal commands and config files.";
|
|
14227
|
+
return null;
|
|
14228
|
+
}
|
|
14229
|
+
|
|
13756
14230
|
// src/app.ts
|
|
13757
14231
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
13758
14232
|
import { fileURLToPath } from "url";
|
|
@@ -13802,7 +14276,10 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
13802
14276
|
c.set("nestScope", null);
|
|
13803
14277
|
return next();
|
|
13804
14278
|
}
|
|
13805
|
-
return c.json(
|
|
14279
|
+
return c.json(
|
|
14280
|
+
{ error: "Authentication required \u2014 sign in or provide an API key.", code: "missing_credentials" },
|
|
14281
|
+
401
|
|
14282
|
+
);
|
|
13806
14283
|
});
|
|
13807
14284
|
function nestPathParts(path) {
|
|
13808
14285
|
return path.replace(/^\/nests\//, "").split("/").filter(Boolean);
|
|
@@ -13820,14 +14297,24 @@ var flexAuthWithPromptowlTicket = createMiddleware2(
|
|
|
13820
14297
|
return flexAuthMiddleware(c, next);
|
|
13821
14298
|
}
|
|
13822
14299
|
let claims = null;
|
|
14300
|
+
let expired = false;
|
|
13823
14301
|
for (const secret of secrets) {
|
|
13824
14302
|
try {
|
|
13825
14303
|
claims = verifySsoTicket(token, secret);
|
|
13826
14304
|
break;
|
|
13827
|
-
} catch {
|
|
14305
|
+
} catch (err) {
|
|
14306
|
+
if (err instanceof SsoTokenError && err.message === "Ticket expired") {
|
|
14307
|
+
expired = true;
|
|
14308
|
+
}
|
|
13828
14309
|
}
|
|
13829
14310
|
}
|
|
13830
14311
|
if (!claims) {
|
|
14312
|
+
if (expired) {
|
|
14313
|
+
return c.json(
|
|
14314
|
+
{ error: "Service ticket expired \u2014 reconnect to refresh.", code: "ticket_expired" },
|
|
14315
|
+
401
|
|
14316
|
+
);
|
|
14317
|
+
}
|
|
13831
14318
|
return c.json({ error: "Invalid service ticket" }, 401);
|
|
13832
14319
|
}
|
|
13833
14320
|
if (claims.scope !== "mcp" || claims.jti) {
|
|
@@ -13874,7 +14361,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
13874
14361
|
return ok;
|
|
13875
14362
|
}
|
|
13876
14363
|
function createApp() {
|
|
13877
|
-
const app = new
|
|
14364
|
+
const app = new Hono26({ router: new LinearRouter() });
|
|
13878
14365
|
const corsOrigins = config.CORS_ORIGINS;
|
|
13879
14366
|
app.use(
|
|
13880
14367
|
"*",
|
|
@@ -13892,7 +14379,8 @@ function createApp() {
|
|
|
13892
14379
|
const lenStr = c.req.header("Content-Length");
|
|
13893
14380
|
if (lenStr) {
|
|
13894
14381
|
const len = parseInt(lenStr, 10);
|
|
13895
|
-
const
|
|
14382
|
+
const isPost = c.req.method === "POST";
|
|
14383
|
+
const cap = isPost && ASSET_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, ASSET_MAX_REQUEST_BYTES) : isPost && PDF_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, pdfMaxRequestBytes()) : config.MAX_BODY_BYTES;
|
|
13896
14384
|
if (Number.isFinite(len) && len > cap) {
|
|
13897
14385
|
return c.json(
|
|
13898
14386
|
{
|
|
@@ -13917,7 +14405,10 @@ function createApp() {
|
|
|
13917
14405
|
nestId: /^\/nests\/([^/]+)/.exec(path)?.[1] ?? null,
|
|
13918
14406
|
userId: c.get("userId") ?? null,
|
|
13919
14407
|
status: c.res.status,
|
|
13920
|
-
durationMs: Date.now() - started
|
|
14408
|
+
durationMs: Date.now() - started,
|
|
14409
|
+
// Set by whichever handler validated the request's `client` block
|
|
14410
|
+
// (§9.4). Absent on a request that sent none.
|
|
14411
|
+
client: c.get("clientMetadata")
|
|
13921
14412
|
});
|
|
13922
14413
|
});
|
|
13923
14414
|
app.get("/health", (c) => {
|
|
@@ -13962,6 +14453,11 @@ function createApp() {
|
|
|
13962
14453
|
// API would refuse it — existing nodes of the type stay readable.
|
|
13963
14454
|
type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
|
|
13964
14455
|
type_table_enabled: config.TYPE_TABLE_ENABLED,
|
|
14456
|
+
// This server hosts several organizations (PromptOwl-hosted) rather than
|
|
14457
|
+
// one. The visibility picker words 'Organization' and 'Public' by it and
|
|
14458
|
+
// confirms before 'org'; the server enforces the same in
|
|
14459
|
+
// PATCH /nests/:id/visibility.
|
|
14460
|
+
shared_deployment: config.SHARED_DEPLOYMENT,
|
|
13965
14461
|
// Sub-agent runs gate its own recursion surface separately (env-only,
|
|
13966
14462
|
// must clear a security review — NOT a UI toggle). Exposed read-only so
|
|
13967
14463
|
// the Runs page shows the "spawn sub-agent" option only when it's live.
|
|
@@ -14085,6 +14581,7 @@ function createApp() {
|
|
|
14085
14581
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
14086
14582
|
oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
|
|
14087
14583
|
people_suggest_all_users: config.PEOPLE_SUGGEST_ALL_USERS,
|
|
14584
|
+
shared_deployment: config.SHARED_DEPLOYMENT,
|
|
14088
14585
|
// Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
|
|
14089
14586
|
// presence is reported, never the value (same discipline as the OIDC secret).
|
|
14090
14587
|
sso_token_exchange_enabled: config.SSO_TOKEN_EXCHANGE_ENABLED,
|
|
@@ -14137,8 +14634,8 @@ function createApp() {
|
|
|
14137
14634
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
14138
14635
|
}
|
|
14139
14636
|
const fieldErrors = {};
|
|
14140
|
-
const addError = (
|
|
14141
|
-
fieldErrors[
|
|
14637
|
+
const addError = (field2, message) => {
|
|
14638
|
+
fieldErrors[field2] = message;
|
|
14142
14639
|
};
|
|
14143
14640
|
const pending = [];
|
|
14144
14641
|
if ("promptowl_sign_in_gate" in body) {
|
|
@@ -14159,7 +14656,9 @@ function createApp() {
|
|
|
14159
14656
|
}
|
|
14160
14657
|
if ("public_base_url" in body) {
|
|
14161
14658
|
const v = String(body.public_base_url ?? "").trim().replace(/\/$/, "");
|
|
14162
|
-
|
|
14659
|
+
const err = v ? publicBaseUrlError(v) : null;
|
|
14660
|
+
if (err) addError("public_base_url", err);
|
|
14661
|
+
else pending.push({ name: "PUBLIC_BASE_URL", value: v || null });
|
|
14163
14662
|
}
|
|
14164
14663
|
if ("telemetry_enabled" in body) {
|
|
14165
14664
|
pending.push({ name: "TELEMETRY_ENABLED", value: body.telemetry_enabled ? "true" : "false" });
|
|
@@ -14313,6 +14812,12 @@ function createApp() {
|
|
|
14313
14812
|
value: body.people_suggest_all_users ? "true" : null
|
|
14314
14813
|
});
|
|
14315
14814
|
}
|
|
14815
|
+
if ("shared_deployment" in body) {
|
|
14816
|
+
pending.push({
|
|
14817
|
+
name: "SHARED_DEPLOYMENT",
|
|
14818
|
+
value: body.shared_deployment ? "true" : null
|
|
14819
|
+
});
|
|
14820
|
+
}
|
|
14316
14821
|
if ("sso_token_exchange_enabled" in body) {
|
|
14317
14822
|
pending.push({
|
|
14318
14823
|
name: "SSO_TOKEN_EXCHANGE_ENABLED",
|
|
@@ -14363,13 +14868,13 @@ function createApp() {
|
|
|
14363
14868
|
const effAllowedDomains = "oidc_allowed_domains" in body ? String(body.oidc_allowed_domains ?? "").split(",").map((d) => d.trim()).filter(Boolean) : config.OIDC_ALLOWED_DOMAINS;
|
|
14364
14869
|
const effAutoProvision = "oidc_auto_provision" in body ? !!body.oidc_auto_provision : config.OIDC_AUTO_PROVISION;
|
|
14365
14870
|
if (effAutoProvision && effAllowedDomains.length === 0) {
|
|
14366
|
-
for (const
|
|
14871
|
+
for (const field2 of [
|
|
14367
14872
|
"ms_token_exchange_enabled",
|
|
14368
14873
|
"google_token_exchange_enabled"
|
|
14369
14874
|
]) {
|
|
14370
|
-
if (body[
|
|
14875
|
+
if (body[field2])
|
|
14371
14876
|
addError(
|
|
14372
|
-
|
|
14877
|
+
field2,
|
|
14373
14878
|
"Set allowed email domains (or turn auto-provision off) before enabling token exchange \u2014 otherwise anyone with an account at that provider can sign themselves up and mint a token."
|
|
14374
14879
|
);
|
|
14375
14880
|
}
|
|
@@ -14819,7 +15324,7 @@ function createApp() {
|
|
|
14819
15324
|
app.route("/index", nestIndexRoutes);
|
|
14820
15325
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
14821
15326
|
app.route("/mcp", serverMcpRoutes);
|
|
14822
|
-
const nestsApp = new
|
|
15327
|
+
const nestsApp = new Hono26();
|
|
14823
15328
|
nestsApp.use("*", async (c, next) => {
|
|
14824
15329
|
const parts = nestPathParts(c.req.path);
|
|
14825
15330
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -14976,7 +15481,9 @@ function createApp() {
|
|
|
14976
15481
|
stewardEditorBypass = resolved.some(
|
|
14977
15482
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor"
|
|
14978
15483
|
);
|
|
14979
|
-
} else if (
|
|
15484
|
+
} else if (c.req.method === "POST" && // POST /nodes (create) and POST /nodes/pdf (PDF upload — a create, or
|
|
15485
|
+
// a new version of a pdf node) share the nest-scope editor rule.
|
|
15486
|
+
(parts.length === 2 || parts.length === 3 && parts[2] === "pdf")) {
|
|
14980
15487
|
const resolved = await resolveStewardsForNode(nestId, "");
|
|
14981
15488
|
stewardEditorBypass = resolved.some(
|
|
14982
15489
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor" && r.steward.scope === "nest"
|
|
@@ -15038,6 +15545,7 @@ function createApp() {
|
|
|
15038
15545
|
});
|
|
15039
15546
|
nestsApp.route("/", nestRoutes);
|
|
15040
15547
|
nestsApp.route("/:nestId", governanceRoutes);
|
|
15548
|
+
nestsApp.route("/:nestId/nodes", pdfNodeRoutes);
|
|
15041
15549
|
nestsApp.route("/:nestId/nodes", annotationRoutes);
|
|
15042
15550
|
nestsApp.route("/:nestId/nodes", governanceNodeRoutes);
|
|
15043
15551
|
nestsApp.route("/:nestId/nodes", nodeRoutes);
|