@promptowl/contextnest-community 1.26.0 → 1.27.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 +34 -1
- package/CONFIGURATION.md +32 -1
- package/dist/{chunk-TRSGPS27.js → chunk-H3W72HN3.js} +28 -0
- package/dist/{chunk-DGPTJXIV.js → chunk-MB52DWWP.js} +4 -4
- package/dist/{chunk-IEAWRHMV.js → chunk-NFFUZF2B.js} +23 -2
- package/dist/{chunk-PICUHQEM.js → chunk-O3JEI33N.js} +2 -2
- package/dist/{chunk-SWA5M5XX.js → chunk-QMQDWRHN.js} +128 -22
- package/dist/{chunk-I5EDEZBQ.js → chunk-RXCVXG34.js} +1 -1
- package/dist/{chunk-2QOJE7A5.js → chunk-SE5U4J4T.js} +4 -4
- package/dist/{chunk-RPGFHESR.js → chunk-VHEMGQ3I.js} +1 -1
- package/dist/{chunk-M6PGYMBD.js → chunk-VRLAP4HB.js} +33 -15
- package/dist/{engine-QNMFDIBV.js → engine-HLYQLP5T.js} +3 -3
- package/dist/{external-edit-service-4MEGPT4D.js → external-edit-service-3DFQ4EZZ.js} +4 -4
- package/dist/{grants-service-24GLUBY4.js → grants-service-WICV4Y2J.js} +2 -2
- package/dist/index.js +804 -304
- package/dist/{invited-user-AM6A3WGO.js → invited-user-XEBX6BM4.js} +3 -3
- package/dist/{review-service-2EWN7WWO.js → review-service-GB43UQVH.js} +7 -7
- package/dist/{stewardship-service-TDZUC2Q6.js → stewardship-service-KDBPWRZI.js} +4 -4
- package/dist/{version-service-C3JTZLJH.js → version-service-JYHFPSYP.js} +8 -4
- package/dist/web3/assets/{ActivityTracePage-DzwWIjjx.js → ActivityTracePage-CkRoyIc-.js} +1 -1
- package/dist/web3/assets/AgentDocsPage-BTRXmliv.js +1 -0
- package/dist/web3/assets/CollaboratorManager-CqJIkvs-.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-JcO1P-qm.js +1 -0
- package/dist/web3/assets/DocumentEditor-D1GLeeEP.js +36 -0
- package/dist/web3/assets/DocumentsTab-BAgSxTX1.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-DuuTXZap.js +1 -0
- package/dist/web3/assets/{MarkdownEditor-CKh4WSn-.js → MarkdownEditor-BELFww9q.js} +116 -126
- package/dist/web3/assets/{NestPageHeader-Cizo8nUi.js → NestPageHeader-Bbje2ncY.js} +1 -1
- package/dist/web3/assets/NestView-D4OMXPQ7.js +73 -0
- package/dist/web3/assets/OverviewTab-FIDGjR3Q.js +6 -0
- package/dist/web3/assets/{PersonCombobox-BKI7dff4.js → PersonCombobox-D52m_jhc.js} +1 -1
- package/dist/web3/assets/{ReviewActions-CS8JXgXF.js → ReviewActions-DYqBcWlv.js} +2 -2
- package/dist/web3/assets/ReviewTab-DoxfygOb.js +1 -0
- package/dist/web3/assets/StewardsTab-CFsYjbEn.js +1 -0
- package/dist/web3/assets/{SubmitForReviewModal-Biq3m79i.js → SubmitForReviewModal-AW2OLNAE.js} +1 -1
- package/dist/web3/assets/{alert-dialog-BvF3NFr7.js → alert-dialog-p-fBLSWx.js} +2 -2
- package/dist/web3/assets/{arrow-left-CWIsv-sG.js → arrow-left-CiErCCTG.js} +1 -1
- package/dist/web3/assets/{backlinks-Cqcn7n1J.js → backlinks-DaVSYvhV.js} +2 -2
- package/dist/web3/assets/{boxes-ClbgQqG2.js → boxes-VWrCCMzW.js} +1 -1
- package/dist/web3/assets/{chevron-left-CK_ZPFm9.js → chevron-left-CsD8gZwL.js} +1 -1
- package/dist/web3/assets/{circle-check-Bw_aTXiH.js → circle-check-BBCXd9LI.js} +1 -1
- package/dist/web3/assets/{circle-x-DkIG68H7.js → circle-x-C9LsnORQ.js} +1 -1
- package/dist/web3/assets/{code-xml-BlR7EN6l.js → code-xml-BaoV7HiQ.js} +1 -1
- package/dist/web3/assets/{corner-down-right-DSVxK3tf.js → corner-down-right-DgiTXrtw.js} +1 -1
- package/dist/web3/assets/{count-skeleton-DToLM74B.js → count-skeleton-CnEE4uF-.js} +1 -1
- package/dist/web3/assets/{earth-BXhVPvCR.js → earth-BmMbVt9k.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-BevGZgtj.js → file-exclamation-point-qJeca9-A.js} +1 -1
- package/dist/web3/assets/{folder-input-m8UBV4fA.js → folder-input-D7R93LJ6.js} +1 -1
- package/dist/web3/assets/index-DNmWTxN7.css +1 -0
- package/dist/web3/assets/index-DvuUIUP6.js +451 -0
- package/dist/web3/assets/{page-DzKwP5Ds.js → page-BJQuaTRW.js} +2 -2
- package/dist/web3/assets/{page-B6pkc9cd.js → page-Bb8xGOPQ.js} +2 -2
- package/dist/web3/assets/{page-Deq6IKiN.js → page-BtM-qDzr.js} +2 -2
- package/dist/web3/assets/{page-DPY01SOj.js → page-Bxd0EG25.js} +1 -1
- package/dist/web3/assets/page-C3QCO6IR.js +1 -0
- package/dist/web3/assets/{page-CptuAPJc.js → page-CfoR0khy.js} +2 -2
- package/dist/web3/assets/page-D7Zr5tAI.js +1 -0
- package/dist/web3/assets/{page-Dwbp4Seu.js → page-DATSlHCW.js} +2 -2
- package/dist/web3/assets/page-DTjPQizZ.js +45 -0
- package/dist/web3/assets/{page-Bgy9Ng9p.js → page-DspptlWr.js} +1 -1
- package/dist/web3/assets/{page-Dh57L0RG.js → page-I6vAFJOj.js} +4 -4
- package/dist/web3/assets/page-QxI4Ud2s.js +9 -0
- package/dist/web3/assets/{page-BJrFCKXB.js → page-YcNNg-dX.js} +1 -1
- package/dist/web3/assets/page-kM7eO5Kj.js +1 -0
- package/dist/web3/assets/{page-title-DVq4qvMR.js → page-title-Dn5jANiU.js} +1 -1
- package/dist/web3/assets/{play-CXhS6Pgq.js → play-B2k_Ix-L.js} +1 -1
- package/dist/web3/assets/{send-DdfwnIrj.js → send-DPNxv3As.js} +1 -1
- package/dist/web3/assets/{settings-DKX0Q4gO.js → settings-DZjq-OQA.js} +1 -1
- package/dist/web3/assets/{share-2-CxmXRt58.js → share-2-C-OnE2ru.js} +1 -1
- package/dist/web3/assets/{tag-DeYC_mRv.js → tag-CwJl7FdI.js} +1 -1
- package/dist/web3/assets/{trash-2-B0HyPTnI.js → trash-2-aljmIff4.js} +1 -1
- package/dist/web3/assets/{user-plus-Ckm_Isyc.js → user-plus-DPerZexX.js} +1 -1
- package/dist/web3/assets/{zap-D2ifcnYb.js → zap-CHZtiN7P.js} +1 -1
- package/dist/web3/index.html +2 -2
- package/package.json +6 -2
- package/dist/web3/assets/AgentDocsPage-ixP2UdAE.js +0 -1
- package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +0 -1
- package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +0 -36
- package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +0 -1
- package/dist/web3/assets/NestView-EW-sraEb.js +0 -73
- package/dist/web3/assets/OverviewTab-CjidsMX_.js +0 -6
- package/dist/web3/assets/ReviewTab-CwVrXzw5.js +0 -1
- package/dist/web3/assets/StewardsTab-C-iP6gnh.js +0 -1
- package/dist/web3/assets/index-BOVYgMvj.js +0 -29
- package/dist/web3/assets/index-DVnaYqj9.css +0 -1
- package/dist/web3/assets/index-WYDmEOfO.js +0 -404
- package/dist/web3/assets/page-C2zoxumw.js +0 -1
- package/dist/web3/assets/page-CJWO06dN.js +0 -1
- package/dist/web3/assets/page-Dea2SblF.js +0 -9
- package/dist/web3/assets/page-Dfjg8p7Z.js +0 -1
- package/dist/web3/assets/page-YxjrE5b5.js +0 -45
- package/dist/web3/assets/x-mNtADYHc.js +0 -6
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-VRLAP4HB.js";
|
|
42
42
|
import {
|
|
43
43
|
canCreateInNest,
|
|
44
44
|
canManageStewards,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
stewardDocumentCreator,
|
|
60
60
|
syncFromConfig,
|
|
61
61
|
updateSteward
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-MB52DWWP.js";
|
|
63
63
|
import {
|
|
64
64
|
createGrant,
|
|
65
65
|
deleteGrant,
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
listGrants,
|
|
69
69
|
listUserGrants,
|
|
70
70
|
resolveNodeGrant
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-VHEMGQ3I.js";
|
|
72
72
|
import {
|
|
73
73
|
attributeRequest,
|
|
74
74
|
checkConflict,
|
|
@@ -88,11 +88,12 @@ import {
|
|
|
88
88
|
getVersions,
|
|
89
89
|
listMyDrafts,
|
|
90
90
|
mergeClient,
|
|
91
|
+
moveApprovedVersion,
|
|
91
92
|
parseClientMetadata,
|
|
92
93
|
serializeClient,
|
|
93
94
|
setApprovedVersion,
|
|
94
95
|
upsertVersion
|
|
95
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-NFFUZF2B.js";
|
|
96
97
|
import {
|
|
97
98
|
approveExternalEdit,
|
|
98
99
|
getExternalEditDetail,
|
|
@@ -104,7 +105,7 @@ import {
|
|
|
104
105
|
scanDocumentForDrift,
|
|
105
106
|
scanNestForDrift,
|
|
106
107
|
startDriftScanner
|
|
107
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-SE5U4J4T.js";
|
|
108
109
|
import {
|
|
109
110
|
addMember,
|
|
110
111
|
addWatcher,
|
|
@@ -205,7 +206,7 @@ import {
|
|
|
205
206
|
unshareTeamFromNest,
|
|
206
207
|
updateMemberRole,
|
|
207
208
|
validateLicense
|
|
208
|
-
} from "./chunk-
|
|
209
|
+
} from "./chunk-QMQDWRHN.js";
|
|
209
210
|
import {
|
|
210
211
|
AppError,
|
|
211
212
|
ConflictError,
|
|
@@ -228,7 +229,7 @@ import {
|
|
|
228
229
|
parseBearerToken,
|
|
229
230
|
verifyKeyRequest,
|
|
230
231
|
verifyPassword
|
|
231
|
-
} from "./chunk-
|
|
232
|
+
} from "./chunk-O3JEI33N.js";
|
|
232
233
|
import {
|
|
233
234
|
canApproveWith,
|
|
234
235
|
canEditWith,
|
|
@@ -238,14 +239,14 @@ import {
|
|
|
238
239
|
sendEmailToRecipient,
|
|
239
240
|
transportSettings,
|
|
240
241
|
verifyEmailTransport
|
|
241
|
-
} from "./chunk-
|
|
242
|
+
} from "./chunk-RXCVXG34.js";
|
|
242
243
|
import {
|
|
243
244
|
config,
|
|
244
245
|
getDb,
|
|
245
246
|
initDb,
|
|
246
247
|
isEmailListish,
|
|
247
248
|
isEmailish
|
|
248
|
-
} from "./chunk-
|
|
249
|
+
} from "./chunk-H3W72HN3.js";
|
|
249
250
|
import {
|
|
250
251
|
ANON_EMAIL,
|
|
251
252
|
ANON_USER_ID,
|
|
@@ -259,7 +260,7 @@ import {
|
|
|
259
260
|
import { serve } from "@hono/node-server";
|
|
260
261
|
|
|
261
262
|
// src/app.ts
|
|
262
|
-
import { Hono as
|
|
263
|
+
import { Hono as Hono28 } from "hono";
|
|
263
264
|
import { LinearRouter } from "hono/router/linear-router";
|
|
264
265
|
import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
265
266
|
import { cors } from "hono/cors";
|
|
@@ -802,9 +803,9 @@ function isOidcReady() {
|
|
|
802
803
|
}
|
|
803
804
|
async function discover(issuer) {
|
|
804
805
|
const now = Date.now();
|
|
805
|
-
const
|
|
806
|
-
if (
|
|
807
|
-
return { doc:
|
|
806
|
+
const cached2 = discoveryCache.get(issuer);
|
|
807
|
+
if (cached2 && now - cached2.fetchedAt < DISCOVERY_TTL_MS) {
|
|
808
|
+
return { doc: cached2.doc, jwks: cached2.jwks };
|
|
808
809
|
}
|
|
809
810
|
const res = await fetch(`${issuer}/.well-known/openid-configuration`, {
|
|
810
811
|
headers: { Accept: "application/json" },
|
|
@@ -2164,6 +2165,35 @@ function stripUndefinedDeep(value) {
|
|
|
2164
2165
|
// src/nodes/service.ts
|
|
2165
2166
|
import { inputJsonSchema } from "@promptowl/contextnest-engine/api";
|
|
2166
2167
|
|
|
2168
|
+
// src/governance/approved-body.ts
|
|
2169
|
+
async function reconstructApproved(versionManager, nestId, nodeId, approved) {
|
|
2170
|
+
try {
|
|
2171
|
+
return { raw: await versionManager.reconstructVersion(nodeId, approved), version: approved };
|
|
2172
|
+
} catch (err) {
|
|
2173
|
+
const current = await getApprovedVersion(nestId, nodeId);
|
|
2174
|
+
if (current != null && current !== approved) {
|
|
2175
|
+
return { raw: await versionManager.reconstructVersion(nodeId, current), version: current };
|
|
2176
|
+
}
|
|
2177
|
+
const newer = (await getVersions(nestId, nodeId)).filter(
|
|
2178
|
+
(v) => v.version > approved && (v.status === "published" || v.status === "approved")
|
|
2179
|
+
);
|
|
2180
|
+
for (const { version } of newer) {
|
|
2181
|
+
let raw;
|
|
2182
|
+
try {
|
|
2183
|
+
raw = await versionManager.reconstructVersion(nodeId, version);
|
|
2184
|
+
} catch {
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
if (await moveApprovedVersion(nestId, nodeId, approved, version)) {
|
|
2188
|
+
console.warn(`[approved-heal] ${nestId}/${nodeId}: approved v${approved} \u2192 v${version} (v${approved} could not be rebuilt)`);
|
|
2189
|
+
}
|
|
2190
|
+
return { raw, version };
|
|
2191
|
+
}
|
|
2192
|
+
console.error(`[approved-heal] ${nestId}/${nodeId}: v${approved} can't be rebuilt and no newer published version can`, err);
|
|
2193
|
+
throw err;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2167
2197
|
// src/governance/prime-service.ts
|
|
2168
2198
|
async function isPrimeDocument(nestId, tags, metadata) {
|
|
2169
2199
|
const flag = documentPrimeFlag(metadata);
|
|
@@ -2204,6 +2234,21 @@ function extractMentions(markdown2) {
|
|
|
2204
2234
|
}
|
|
2205
2235
|
return names;
|
|
2206
2236
|
}
|
|
2237
|
+
function stripMentions(text) {
|
|
2238
|
+
const spans = [];
|
|
2239
|
+
const code = (text || "").replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length)).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length));
|
|
2240
|
+
let match;
|
|
2241
|
+
MENTION.lastIndex = 0;
|
|
2242
|
+
while ((match = MENTION.exec(code)) !== null) {
|
|
2243
|
+
const lead = match[0].startsWith("@") ? 0 : 1;
|
|
2244
|
+
spans.push([match.index + lead, match.index + match[0].length]);
|
|
2245
|
+
}
|
|
2246
|
+
let out = text || "";
|
|
2247
|
+
for (const [start, end] of spans.reverse()) {
|
|
2248
|
+
out = out.slice(0, start) + out.slice(end);
|
|
2249
|
+
}
|
|
2250
|
+
return out.replace(/\s+/g, " ").trim();
|
|
2251
|
+
}
|
|
2207
2252
|
function newMentions(previous, next) {
|
|
2208
2253
|
const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
|
|
2209
2254
|
return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
|
|
@@ -2973,13 +3018,16 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2973
3018
|
r.myGrant = grantRoleForNode(myGrants, doc.id);
|
|
2974
3019
|
}
|
|
2975
3020
|
if (approvedVersions) {
|
|
2976
|
-
|
|
3021
|
+
let approved = approvedVersions.get(doc.id);
|
|
2977
3022
|
if (approved != null) {
|
|
2978
3023
|
try {
|
|
2979
|
-
|
|
3024
|
+
let raw;
|
|
3025
|
+
({ raw, version: approved } = await reconstructApproved(
|
|
3026
|
+
versionManager,
|
|
3027
|
+
nestId,
|
|
2980
3028
|
doc.id,
|
|
2981
3029
|
approved
|
|
2982
|
-
);
|
|
3030
|
+
));
|
|
2983
3031
|
r.content = bodyOnly(doc.id, raw);
|
|
2984
3032
|
} catch (err) {
|
|
2985
3033
|
console.error(
|
|
@@ -5281,7 +5329,7 @@ async function requireManage(c, nestId, opts = {}) {
|
|
|
5281
5329
|
}
|
|
5282
5330
|
}
|
|
5283
5331
|
async function resolveGrantee(email) {
|
|
5284
|
-
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-
|
|
5332
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-XEBX6BM4.js");
|
|
5285
5333
|
return (await findOrCreateInvitedUser2(email)).id;
|
|
5286
5334
|
}
|
|
5287
5335
|
async function assertGrantAddsAccess(params) {
|
|
@@ -7017,7 +7065,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
7017
7065
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
7018
7066
|
const nestId = c.req.param("nestId");
|
|
7019
7067
|
const nodeId = c.req.param("nodeId");
|
|
7020
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
7068
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-KDBPWRZI.js");
|
|
7021
7069
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
7022
7070
|
nestId,
|
|
7023
7071
|
nodeId
|
|
@@ -7038,7 +7086,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
7038
7086
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
7039
7087
|
const nestId = c.req.param("nestId");
|
|
7040
7088
|
const nodeId = c.req.param("nodeId");
|
|
7041
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
7089
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-GB43UQVH.js");
|
|
7042
7090
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
7043
7091
|
return c.json({ reviews: history });
|
|
7044
7092
|
});
|
|
@@ -7194,14 +7242,16 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
7194
7242
|
response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
|
|
7195
7243
|
const approvedOnly = c.req.query("approved_only") === "1" || c.req.query("approved_only") === "true";
|
|
7196
7244
|
if (publicReader || approvedOnly) {
|
|
7197
|
-
|
|
7245
|
+
let approved = await getApprovedVersion(nestId, nodeId);
|
|
7198
7246
|
if (approved != null) {
|
|
7199
7247
|
let approvedRaw = "";
|
|
7200
7248
|
try {
|
|
7201
|
-
approvedRaw = await
|
|
7249
|
+
({ raw: approvedRaw, version: approved } = await reconstructApproved(
|
|
7250
|
+
versionManager,
|
|
7251
|
+
nestId,
|
|
7202
7252
|
nodeId,
|
|
7203
7253
|
approved
|
|
7204
|
-
);
|
|
7254
|
+
));
|
|
7205
7255
|
response.content = bodyOnly(nodeId, approvedRaw);
|
|
7206
7256
|
if (response.type === "pdf") {
|
|
7207
7257
|
const approvedPdf = pdfMetaOf(nodeId, approvedRaw);
|
|
@@ -7890,7 +7940,7 @@ async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
|
|
|
7890
7940
|
if (approved == null) return "";
|
|
7891
7941
|
try {
|
|
7892
7942
|
const { versions } = await engineCache.get(nestId);
|
|
7893
|
-
const raw = await versions
|
|
7943
|
+
const { raw } = await reconstructApproved(versions, nestId, nodeId, approved);
|
|
7894
7944
|
return bodyOnly(nodeId, raw);
|
|
7895
7945
|
} catch {
|
|
7896
7946
|
return "";
|
|
@@ -7902,75 +7952,13 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
7902
7952
|
if (approved == null) return null;
|
|
7903
7953
|
try {
|
|
7904
7954
|
const { versions } = await engineCache.get(nestId);
|
|
7905
|
-
const raw = await versions
|
|
7955
|
+
const { raw } = await reconstructApproved(versions, nestId, nodeId, approved);
|
|
7906
7956
|
return bodyOnly(nodeId, raw);
|
|
7907
7957
|
} catch {
|
|
7908
7958
|
return null;
|
|
7909
7959
|
}
|
|
7910
7960
|
}
|
|
7911
7961
|
|
|
7912
|
-
// src/nests/bundle-service.ts
|
|
7913
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
7914
|
-
import { join as join5, sep } from "path";
|
|
7915
|
-
import JSZip from "jszip";
|
|
7916
|
-
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
7917
|
-
function walkFiles(root) {
|
|
7918
|
-
const out = [];
|
|
7919
|
-
let entries;
|
|
7920
|
-
try {
|
|
7921
|
-
entries = readdirSync2(root, { recursive: true });
|
|
7922
|
-
} catch {
|
|
7923
|
-
return out;
|
|
7924
|
-
}
|
|
7925
|
-
for (const rel of entries) {
|
|
7926
|
-
try {
|
|
7927
|
-
if (statSync2(join5(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
7928
|
-
} catch {
|
|
7929
|
-
}
|
|
7930
|
-
}
|
|
7931
|
-
return out;
|
|
7932
|
-
}
|
|
7933
|
-
async function exportNestBundle(nestId, includeDrafts) {
|
|
7934
|
-
const db = getDb();
|
|
7935
|
-
const approvedRows = await db.all(
|
|
7936
|
-
"SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
|
|
7937
|
-
[nestId]
|
|
7938
|
-
);
|
|
7939
|
-
const approvedByNode = new Map(
|
|
7940
|
-
approvedRows.map((r) => [r.node_id, r.approved_version])
|
|
7941
|
-
);
|
|
7942
|
-
const { storage } = await engineCache.get(nestId);
|
|
7943
|
-
const docs = await storage.discoverDocuments();
|
|
7944
|
-
const zip = new JSZip();
|
|
7945
|
-
let count = 0;
|
|
7946
|
-
for (const doc of docs) {
|
|
7947
|
-
const approved = approvedByNode.get(doc.id);
|
|
7948
|
-
const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
|
|
7949
|
-
let body = doc.body || "";
|
|
7950
|
-
if (approved != null) {
|
|
7951
|
-
const approvedBody = await resolveExportBody(nestId, doc.id, body);
|
|
7952
|
-
if (approvedBody == null) continue;
|
|
7953
|
-
body = approvedBody;
|
|
7954
|
-
frontmatter.status = "published";
|
|
7955
|
-
frontmatter.version = approved;
|
|
7956
|
-
} else {
|
|
7957
|
-
if (!includeDrafts) continue;
|
|
7958
|
-
frontmatter.status = "draft";
|
|
7959
|
-
}
|
|
7960
|
-
zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
|
|
7961
|
-
count++;
|
|
7962
|
-
}
|
|
7963
|
-
const nestPath = resolveNestPath(nestId);
|
|
7964
|
-
for (const rel of walkFiles(nestPath)) {
|
|
7965
|
-
const segs = rel.split("/");
|
|
7966
|
-
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
7967
|
-
if (!keep) continue;
|
|
7968
|
-
zip.file(rel, readFileSync2(join5(nestPath, rel)));
|
|
7969
|
-
}
|
|
7970
|
-
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
7971
|
-
return zip.generateAsync({ type: "arraybuffer" });
|
|
7972
|
-
}
|
|
7973
|
-
|
|
7974
7962
|
// src/nodes/graph-service.ts
|
|
7975
7963
|
import {
|
|
7976
7964
|
contextLinkTarget,
|
|
@@ -8156,132 +8144,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
8156
8144
|
};
|
|
8157
8145
|
}
|
|
8158
8146
|
|
|
8159
|
-
// src/nodes/
|
|
8160
|
-
var queryRoutes = new Hono14();
|
|
8161
|
-
queryRoutes.get("/changes", async (c) => {
|
|
8162
|
-
const nestId = c.req.param("nestId");
|
|
8163
|
-
const userId = c.get("userId");
|
|
8164
|
-
const sinceRaw = c.req.query("since");
|
|
8165
|
-
const sinceMs = sinceRaw ? Date.parse(sinceRaw) : NaN;
|
|
8166
|
-
if (!sinceRaw || Number.isNaN(sinceMs)) {
|
|
8167
|
-
throw new ValidationError(
|
|
8168
|
-
"since must be an ISO-8601 date, e.g. ?since=2026-07-01T00:00:00Z"
|
|
8169
|
-
);
|
|
8170
|
-
}
|
|
8171
|
-
const includeContent = ["1", "true"].includes(
|
|
8172
|
-
c.req.query("include_content") || ""
|
|
8173
|
-
);
|
|
8174
|
-
const all = await listNodesForCaller(nestId, userId, {
|
|
8175
|
-
...includeContent ? { withContent: true } : {}
|
|
8176
|
-
});
|
|
8177
|
-
const db = getDb();
|
|
8178
|
-
const publicReader = await isPublicReader(nestId, userId);
|
|
8179
|
-
const approvedRows = await db.all(
|
|
8180
|
-
"SELECT node_id, approved_at FROM approved_versions WHERE nest_id = ?",
|
|
8181
|
-
[nestId]
|
|
8182
|
-
);
|
|
8183
|
-
const parseDbTs = (t) => Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`);
|
|
8184
|
-
const approvedAtMsByNode = new Map(
|
|
8185
|
-
approvedRows.map((r) => [r.node_id, parseDbTs(r.approved_at)])
|
|
8186
|
-
);
|
|
8187
|
-
const publishedSince = new Set(
|
|
8188
|
-
approvedRows.filter((r) => parseDbTs(r.approved_at) > sinceMs).map((r) => r.node_id)
|
|
8189
|
-
);
|
|
8190
|
-
const summarize = (n, updatedOverride) => ({
|
|
8191
|
-
id: n.id,
|
|
8192
|
-
title: n.title,
|
|
8193
|
-
type: n.type,
|
|
8194
|
-
status: n.status,
|
|
8195
|
-
tags: n.tags,
|
|
8196
|
-
created_at: n.created_at,
|
|
8197
|
-
updated_at: updatedOverride ?? n.updated_at,
|
|
8198
|
-
...includeContent ? { content: n.content } : {}
|
|
8199
|
-
});
|
|
8200
|
-
const created = [];
|
|
8201
|
-
const updated = [];
|
|
8202
|
-
for (const n of all) {
|
|
8203
|
-
if (publicReader) {
|
|
8204
|
-
const approvedMs = approvedAtMsByNode.get(n.id);
|
|
8205
|
-
if (approvedMs === void 0) continue;
|
|
8206
|
-
const approvedIso = new Date(approvedMs).toISOString();
|
|
8207
|
-
const createdAt2 = Date.parse(n.created_at || "");
|
|
8208
|
-
if (!Number.isNaN(createdAt2) && createdAt2 > sinceMs) {
|
|
8209
|
-
created.push(summarize(n, approvedIso));
|
|
8210
|
-
} else if (approvedMs > sinceMs) {
|
|
8211
|
-
updated.push(summarize(n, approvedIso));
|
|
8212
|
-
}
|
|
8213
|
-
continue;
|
|
8214
|
-
}
|
|
8215
|
-
const createdAt = Date.parse(n.created_at || "");
|
|
8216
|
-
const updatedAt = Date.parse(n.updated_at || "");
|
|
8217
|
-
if (!Number.isNaN(createdAt) && createdAt > sinceMs) created.push(summarize(n));
|
|
8218
|
-
else if (!Number.isNaN(updatedAt) && updatedAt > sinceMs) updated.push(summarize(n));
|
|
8219
|
-
else if (publishedSince.has(n.id)) updated.push(summarize(n));
|
|
8220
|
-
}
|
|
8221
|
-
const canSeeDeleted = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
8222
|
-
let deleted = null;
|
|
8223
|
-
if (canSeeDeleted) {
|
|
8224
|
-
const rows = await db.all(
|
|
8225
|
-
"SELECT node_id, deleted_by, deleted_at FROM node_deletions WHERE nest_id = ? AND deleted_at > ?",
|
|
8226
|
-
[nestId, new Date(sinceMs).toISOString()]
|
|
8227
|
-
);
|
|
8228
|
-
const liveIds = new Set(all.map((n) => n.id));
|
|
8229
|
-
deleted = rows.filter((r) => !liveIds.has(r.node_id)).map((r) => ({ id: r.node_id, deleted_by: r.deleted_by, deleted_at: r.deleted_at }));
|
|
8230
|
-
}
|
|
8231
|
-
return c.json({
|
|
8232
|
-
since: new Date(sinceMs).toISOString(),
|
|
8233
|
-
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8234
|
-
created,
|
|
8235
|
-
updated,
|
|
8236
|
-
deleted
|
|
8237
|
-
});
|
|
8238
|
-
});
|
|
8239
|
-
queryRoutes.get("/runnables", async (c) => {
|
|
8240
|
-
const nestId = c.req.param("nestId");
|
|
8241
|
-
const userId = c.get("userId");
|
|
8242
|
-
const all = await listNodesForCaller(nestId, userId, {
|
|
8243
|
-
type: RUNNABLE_NODE_TYPES,
|
|
8244
|
-
// The prompt/skill body IS the payload here — an orchestrator pulls it to
|
|
8245
|
-
// run the thing, so this surface pays for the file reads.
|
|
8246
|
-
withContent: true
|
|
8247
|
-
});
|
|
8248
|
-
const runnables = all.map((n) => ({
|
|
8249
|
-
id: n.id,
|
|
8250
|
-
title: n.title,
|
|
8251
|
-
type: n.type,
|
|
8252
|
-
schedule: n.schedule ?? null,
|
|
8253
|
-
status: n.status,
|
|
8254
|
-
tags: n.tags,
|
|
8255
|
-
updated_at: n.updated_at,
|
|
8256
|
-
content: n.content
|
|
8257
|
-
}));
|
|
8258
|
-
return c.json({ count: runnables.length, runnables });
|
|
8259
|
-
});
|
|
8260
|
-
queryRoutes.get("/graph", async (c) => {
|
|
8261
|
-
const nestId = c.req.param("nestId");
|
|
8262
|
-
const userId = c.get("userId");
|
|
8263
|
-
const userEmail = await resolveCallerEmail(userId);
|
|
8264
|
-
const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
8265
|
-
const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
|
|
8266
|
-
if (config.FEATURE_WORKFLOW_PLANE) {
|
|
8267
|
-
const rows = await getDb().all(
|
|
8268
|
-
`SELECT e.from_node, e.to_node, e.condition_mode, t.name as type, t.color, t.is_flow
|
|
8269
|
-
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
8270
|
-
WHERE e.nest_id = ?`,
|
|
8271
|
-
[nestId]
|
|
8272
|
-
);
|
|
8273
|
-
const visible = new Set(graph.nodes.map((n) => n.id));
|
|
8274
|
-
graph.typedEdges = rows.filter((r) => visible.has(r.from_node) && visible.has(r.to_node)).map((r) => ({
|
|
8275
|
-
source: r.from_node,
|
|
8276
|
-
target: r.to_node,
|
|
8277
|
-
type: r.type,
|
|
8278
|
-
color: r.color,
|
|
8279
|
-
is_flow: !!r.is_flow,
|
|
8280
|
-
condition_mode: r.condition_mode
|
|
8281
|
-
}));
|
|
8282
|
-
}
|
|
8283
|
-
return c.json(graph);
|
|
8284
|
-
});
|
|
8147
|
+
// src/nodes/context-service.ts
|
|
8285
8148
|
function approxTokens(text) {
|
|
8286
8149
|
return Math.ceil(text.length / 4);
|
|
8287
8150
|
}
|
|
@@ -8300,13 +8163,11 @@ ${meta.join("\n")}
|
|
|
8300
8163
|
`;
|
|
8301
8164
|
return header + (node.body || "").trim();
|
|
8302
8165
|
}
|
|
8303
|
-
|
|
8304
|
-
const body =
|
|
8166
|
+
async function resolveContext(params) {
|
|
8167
|
+
const { nestId, userId, baseUrl, body } = params;
|
|
8305
8168
|
if (!body.prompt && !body.selector) {
|
|
8306
8169
|
throw new ValidationError("prompt or selector is required");
|
|
8307
8170
|
}
|
|
8308
|
-
const nestId = c.req.param("nestId");
|
|
8309
|
-
const baseUrl = requestBaseUrl(c.req.url);
|
|
8310
8171
|
const { query: queryEngine, storage } = await engineCache.get(nestId);
|
|
8311
8172
|
const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
|
|
8312
8173
|
const promptPath = !body.selector?.trim() && !!body.prompt;
|
|
@@ -8321,17 +8182,23 @@ queryRoutes.post("/context", async (c) => {
|
|
|
8321
8182
|
if (selector && selector.includes("[[")) {
|
|
8322
8183
|
const parts = selector.split("|").map((p) => p.trim()).filter(Boolean);
|
|
8323
8184
|
const wantedTitles = /* @__PURE__ */ new Set();
|
|
8185
|
+
const wantedIds = /* @__PURE__ */ new Set();
|
|
8324
8186
|
const rest = [];
|
|
8325
8187
|
for (const p of parts) {
|
|
8326
8188
|
const m = /^\[\[(.+)\]\]$/.exec(p);
|
|
8327
|
-
if (m)
|
|
8328
|
-
|
|
8189
|
+
if (!m) {
|
|
8190
|
+
rest.push(p);
|
|
8191
|
+
continue;
|
|
8192
|
+
}
|
|
8193
|
+
const inner = m[1].trim();
|
|
8194
|
+
if (inner.startsWith("nodes/")) wantedIds.add(inner);
|
|
8195
|
+
else wantedTitles.add(inner.toLowerCase());
|
|
8329
8196
|
}
|
|
8330
|
-
if (wantedTitles.size > 0) {
|
|
8197
|
+
if (wantedTitles.size > 0 || wantedIds.size > 0) {
|
|
8331
8198
|
selector = rest.join("|") || null;
|
|
8332
8199
|
resolvedTitleSelector = true;
|
|
8333
8200
|
titleMatches = allDocs.filter(
|
|
8334
|
-
(d) => wantedTitles.has(String(d.frontmatter.title || "").toLowerCase()) && isVisible(d)
|
|
8201
|
+
(d) => (wantedIds.has(d.id) || wantedTitles.has(String(d.frontmatter.title || "").toLowerCase())) && isVisible(d)
|
|
8335
8202
|
);
|
|
8336
8203
|
}
|
|
8337
8204
|
}
|
|
@@ -8436,7 +8303,6 @@ queryRoutes.post("/context", async (c) => {
|
|
|
8436
8303
|
});
|
|
8437
8304
|
documents.sort((a, b) => relevance.get(b.id).score - relevance.get(a.id).score);
|
|
8438
8305
|
}
|
|
8439
|
-
const userId = c.get("userId");
|
|
8440
8306
|
const userEmail = await resolveCallerEmail(userId);
|
|
8441
8307
|
const beforePermission = documents.length;
|
|
8442
8308
|
const accessible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
@@ -8470,8 +8336,13 @@ queryRoutes.post("/context", async (c) => {
|
|
|
8470
8336
|
permission_filtered: permissionFiltered,
|
|
8471
8337
|
token_count: tokenCount
|
|
8472
8338
|
});
|
|
8473
|
-
return
|
|
8339
|
+
return {
|
|
8474
8340
|
context: contextMarkdown,
|
|
8341
|
+
blocks: included.map((n) => ({
|
|
8342
|
+
id: n.id,
|
|
8343
|
+
title: n.frontmatter.title,
|
|
8344
|
+
markdown: formatContextBlock(n, n.url)
|
|
8345
|
+
})),
|
|
8475
8346
|
nodes: included.map((n) => ({
|
|
8476
8347
|
id: n.id,
|
|
8477
8348
|
title: n.frontmatter.title,
|
|
@@ -8498,37 +8369,236 @@ queryRoutes.post("/context", async (c) => {
|
|
|
8498
8369
|
approx_tokens: tokenCount,
|
|
8499
8370
|
max_tokens: maxTokens
|
|
8500
8371
|
}
|
|
8501
|
-
}
|
|
8502
|
-
}
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
const
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
const
|
|
8527
|
-
const
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8372
|
+
};
|
|
8373
|
+
}
|
|
8374
|
+
|
|
8375
|
+
// src/nests/bundle-service.ts
|
|
8376
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
8377
|
+
import { join as join5, sep } from "path";
|
|
8378
|
+
import JSZip from "jszip";
|
|
8379
|
+
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
8380
|
+
function walkFiles(root) {
|
|
8381
|
+
const out = [];
|
|
8382
|
+
let entries;
|
|
8383
|
+
try {
|
|
8384
|
+
entries = readdirSync2(root, { recursive: true });
|
|
8385
|
+
} catch {
|
|
8386
|
+
return out;
|
|
8387
|
+
}
|
|
8388
|
+
for (const rel of entries) {
|
|
8389
|
+
try {
|
|
8390
|
+
if (statSync2(join5(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
8391
|
+
} catch {
|
|
8392
|
+
}
|
|
8393
|
+
}
|
|
8394
|
+
return out;
|
|
8395
|
+
}
|
|
8396
|
+
async function exportNestBundle(nestId, includeDrafts) {
|
|
8397
|
+
const db = getDb();
|
|
8398
|
+
const approvedRows = await db.all(
|
|
8399
|
+
"SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
|
|
8400
|
+
[nestId]
|
|
8401
|
+
);
|
|
8402
|
+
const approvedByNode = new Map(
|
|
8403
|
+
approvedRows.map((r) => [r.node_id, r.approved_version])
|
|
8404
|
+
);
|
|
8405
|
+
const { storage } = await engineCache.get(nestId);
|
|
8406
|
+
const docs = await storage.discoverDocuments();
|
|
8407
|
+
const zip = new JSZip();
|
|
8408
|
+
let count = 0;
|
|
8409
|
+
for (const doc of docs) {
|
|
8410
|
+
const approved = approvedByNode.get(doc.id);
|
|
8411
|
+
const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
|
|
8412
|
+
let body = doc.body || "";
|
|
8413
|
+
if (approved != null) {
|
|
8414
|
+
const approvedBody = await resolveExportBody(nestId, doc.id, body);
|
|
8415
|
+
if (approvedBody == null) continue;
|
|
8416
|
+
body = approvedBody;
|
|
8417
|
+
frontmatter.status = "published";
|
|
8418
|
+
frontmatter.version = approved;
|
|
8419
|
+
} else {
|
|
8420
|
+
if (!includeDrafts) continue;
|
|
8421
|
+
frontmatter.status = "draft";
|
|
8422
|
+
}
|
|
8423
|
+
zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
|
|
8424
|
+
count++;
|
|
8425
|
+
}
|
|
8426
|
+
const nestPath = resolveNestPath(nestId);
|
|
8427
|
+
for (const rel of walkFiles(nestPath)) {
|
|
8428
|
+
const segs = rel.split("/");
|
|
8429
|
+
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
8430
|
+
if (!keep) continue;
|
|
8431
|
+
zip.file(rel, readFileSync2(join5(nestPath, rel)));
|
|
8432
|
+
}
|
|
8433
|
+
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
8434
|
+
return zip.generateAsync({ type: "arraybuffer" });
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8437
|
+
// src/nodes/query-routes.ts
|
|
8438
|
+
var queryRoutes = new Hono14();
|
|
8439
|
+
queryRoutes.get("/changes", async (c) => {
|
|
8440
|
+
const nestId = c.req.param("nestId");
|
|
8441
|
+
const userId = c.get("userId");
|
|
8442
|
+
const sinceRaw = c.req.query("since");
|
|
8443
|
+
const sinceMs = sinceRaw ? Date.parse(sinceRaw) : NaN;
|
|
8444
|
+
if (!sinceRaw || Number.isNaN(sinceMs)) {
|
|
8445
|
+
throw new ValidationError(
|
|
8446
|
+
"since must be an ISO-8601 date, e.g. ?since=2026-07-01T00:00:00Z"
|
|
8447
|
+
);
|
|
8448
|
+
}
|
|
8449
|
+
const includeContent = ["1", "true"].includes(
|
|
8450
|
+
c.req.query("include_content") || ""
|
|
8451
|
+
);
|
|
8452
|
+
const all = await listNodesForCaller(nestId, userId, {
|
|
8453
|
+
...includeContent ? { withContent: true } : {}
|
|
8454
|
+
});
|
|
8455
|
+
const db = getDb();
|
|
8456
|
+
const publicReader = await isPublicReader(nestId, userId);
|
|
8457
|
+
const approvedRows = await db.all(
|
|
8458
|
+
"SELECT node_id, approved_at FROM approved_versions WHERE nest_id = ?",
|
|
8459
|
+
[nestId]
|
|
8460
|
+
);
|
|
8461
|
+
const parseDbTs = (t) => Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`);
|
|
8462
|
+
const approvedAtMsByNode = new Map(
|
|
8463
|
+
approvedRows.map((r) => [r.node_id, parseDbTs(r.approved_at)])
|
|
8464
|
+
);
|
|
8465
|
+
const publishedSince = new Set(
|
|
8466
|
+
approvedRows.filter((r) => parseDbTs(r.approved_at) > sinceMs).map((r) => r.node_id)
|
|
8467
|
+
);
|
|
8468
|
+
const summarize = (n, updatedOverride) => ({
|
|
8469
|
+
id: n.id,
|
|
8470
|
+
title: n.title,
|
|
8471
|
+
type: n.type,
|
|
8472
|
+
status: n.status,
|
|
8473
|
+
tags: n.tags,
|
|
8474
|
+
created_at: n.created_at,
|
|
8475
|
+
updated_at: updatedOverride ?? n.updated_at,
|
|
8476
|
+
...includeContent ? { content: n.content } : {}
|
|
8477
|
+
});
|
|
8478
|
+
const created = [];
|
|
8479
|
+
const updated = [];
|
|
8480
|
+
for (const n of all) {
|
|
8481
|
+
if (publicReader) {
|
|
8482
|
+
const approvedMs = approvedAtMsByNode.get(n.id);
|
|
8483
|
+
if (approvedMs === void 0) continue;
|
|
8484
|
+
const approvedIso = new Date(approvedMs).toISOString();
|
|
8485
|
+
const createdAt2 = Date.parse(n.created_at || "");
|
|
8486
|
+
if (!Number.isNaN(createdAt2) && createdAt2 > sinceMs) {
|
|
8487
|
+
created.push(summarize(n, approvedIso));
|
|
8488
|
+
} else if (approvedMs > sinceMs) {
|
|
8489
|
+
updated.push(summarize(n, approvedIso));
|
|
8490
|
+
}
|
|
8491
|
+
continue;
|
|
8492
|
+
}
|
|
8493
|
+
const createdAt = Date.parse(n.created_at || "");
|
|
8494
|
+
const updatedAt = Date.parse(n.updated_at || "");
|
|
8495
|
+
if (!Number.isNaN(createdAt) && createdAt > sinceMs) created.push(summarize(n));
|
|
8496
|
+
else if (!Number.isNaN(updatedAt) && updatedAt > sinceMs) updated.push(summarize(n));
|
|
8497
|
+
else if (publishedSince.has(n.id)) updated.push(summarize(n));
|
|
8498
|
+
}
|
|
8499
|
+
const canSeeDeleted = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
8500
|
+
let deleted = null;
|
|
8501
|
+
if (canSeeDeleted) {
|
|
8502
|
+
const rows = await db.all(
|
|
8503
|
+
"SELECT node_id, deleted_by, deleted_at FROM node_deletions WHERE nest_id = ? AND deleted_at > ?",
|
|
8504
|
+
[nestId, new Date(sinceMs).toISOString()]
|
|
8505
|
+
);
|
|
8506
|
+
const liveIds = new Set(all.map((n) => n.id));
|
|
8507
|
+
deleted = rows.filter((r) => !liveIds.has(r.node_id)).map((r) => ({ id: r.node_id, deleted_by: r.deleted_by, deleted_at: r.deleted_at }));
|
|
8508
|
+
}
|
|
8509
|
+
return c.json({
|
|
8510
|
+
since: new Date(sinceMs).toISOString(),
|
|
8511
|
+
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8512
|
+
created,
|
|
8513
|
+
updated,
|
|
8514
|
+
deleted
|
|
8515
|
+
});
|
|
8516
|
+
});
|
|
8517
|
+
queryRoutes.get("/runnables", async (c) => {
|
|
8518
|
+
const nestId = c.req.param("nestId");
|
|
8519
|
+
const userId = c.get("userId");
|
|
8520
|
+
const all = await listNodesForCaller(nestId, userId, {
|
|
8521
|
+
type: RUNNABLE_NODE_TYPES,
|
|
8522
|
+
// The prompt/skill body IS the payload here — an orchestrator pulls it to
|
|
8523
|
+
// run the thing, so this surface pays for the file reads.
|
|
8524
|
+
withContent: true
|
|
8525
|
+
});
|
|
8526
|
+
const runnables = all.map((n) => ({
|
|
8527
|
+
id: n.id,
|
|
8528
|
+
title: n.title,
|
|
8529
|
+
type: n.type,
|
|
8530
|
+
schedule: n.schedule ?? null,
|
|
8531
|
+
status: n.status,
|
|
8532
|
+
tags: n.tags,
|
|
8533
|
+
updated_at: n.updated_at,
|
|
8534
|
+
content: n.content
|
|
8535
|
+
}));
|
|
8536
|
+
return c.json({ count: runnables.length, runnables });
|
|
8537
|
+
});
|
|
8538
|
+
queryRoutes.get("/graph", async (c) => {
|
|
8539
|
+
const nestId = c.req.param("nestId");
|
|
8540
|
+
const userId = c.get("userId");
|
|
8541
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
8542
|
+
const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
8543
|
+
const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
|
|
8544
|
+
if (config.FEATURE_WORKFLOW_PLANE) {
|
|
8545
|
+
const rows = await getDb().all(
|
|
8546
|
+
`SELECT e.from_node, e.to_node, e.condition_mode, t.name as type, t.color, t.is_flow
|
|
8547
|
+
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
8548
|
+
WHERE e.nest_id = ?`,
|
|
8549
|
+
[nestId]
|
|
8550
|
+
);
|
|
8551
|
+
const visible = new Set(graph.nodes.map((n) => n.id));
|
|
8552
|
+
graph.typedEdges = rows.filter((r) => visible.has(r.from_node) && visible.has(r.to_node)).map((r) => ({
|
|
8553
|
+
source: r.from_node,
|
|
8554
|
+
target: r.to_node,
|
|
8555
|
+
type: r.type,
|
|
8556
|
+
color: r.color,
|
|
8557
|
+
is_flow: !!r.is_flow,
|
|
8558
|
+
condition_mode: r.condition_mode
|
|
8559
|
+
}));
|
|
8560
|
+
}
|
|
8561
|
+
return c.json(graph);
|
|
8562
|
+
});
|
|
8563
|
+
queryRoutes.post("/context", async (c) => {
|
|
8564
|
+
const body = await c.req.json();
|
|
8565
|
+
const { blocks: _blocks, ...result } = await resolveContext({
|
|
8566
|
+
nestId: c.req.param("nestId"),
|
|
8567
|
+
userId: c.get("userId"),
|
|
8568
|
+
baseUrl: requestBaseUrl(c.req.url),
|
|
8569
|
+
body
|
|
8570
|
+
});
|
|
8571
|
+
return c.json(result);
|
|
8572
|
+
});
|
|
8573
|
+
queryRoutes.post("/table-query", async (c) => {
|
|
8574
|
+
const nestId = c.req.param("nestId");
|
|
8575
|
+
const body = await c.req.json();
|
|
8576
|
+
const ref = typeof body.table === "string" ? body.table.trim() : "";
|
|
8577
|
+
if (!ref) throw new ValidationError("table is required \u2014 [[Title]] or node id");
|
|
8578
|
+
const result = await queryTableNode(
|
|
8579
|
+
nestId,
|
|
8580
|
+
ref,
|
|
8581
|
+
typeof body.query === "string" ? body.query : "",
|
|
8582
|
+
body.include_drafts === true
|
|
8583
|
+
);
|
|
8584
|
+
return c.json(result);
|
|
8585
|
+
});
|
|
8586
|
+
queryRoutes.post("/query", async (c) => {
|
|
8587
|
+
const body = await c.req.json();
|
|
8588
|
+
if (!body.query) {
|
|
8589
|
+
throw new ValidationError("query is required");
|
|
8590
|
+
}
|
|
8591
|
+
const nestId = c.req.param("nestId");
|
|
8592
|
+
const { query: queryEngine } = await engineCache.get(nestId);
|
|
8593
|
+
const result = await queryEngine.query(body.query, {
|
|
8594
|
+
hops: body.hops ?? 2
|
|
8595
|
+
});
|
|
8596
|
+
const userId = c.get("userId");
|
|
8597
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
8598
|
+
const accessible = await filterAccessible(nestId, userId, userEmail, result.documents);
|
|
8599
|
+
return c.json({
|
|
8600
|
+
query: body.query,
|
|
8601
|
+
count: accessible.length,
|
|
8532
8602
|
nodes: accessible.map((n) => ({
|
|
8533
8603
|
id: n.id,
|
|
8534
8604
|
title: n.frontmatter.title,
|
|
@@ -8754,8 +8824,393 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
8754
8824
|
);
|
|
8755
8825
|
});
|
|
8756
8826
|
|
|
8827
|
+
// src/hootie/routes.ts
|
|
8828
|
+
import { Hono as Hono15 } from "hono";
|
|
8829
|
+
import { streamSSE } from "hono/streaming";
|
|
8830
|
+
|
|
8831
|
+
// src/hootie/model.ts
|
|
8832
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
8833
|
+
var plainModels = /* @__PURE__ */ new Set();
|
|
8834
|
+
async function* streamAnswer(params) {
|
|
8835
|
+
const client = clientFor(params.apiKey, params.baseUrl ?? null);
|
|
8836
|
+
const key = `${params.baseUrl ?? ""}|${params.model}`;
|
|
8837
|
+
if (plainModels.has(key)) {
|
|
8838
|
+
yield* answer(client, params, false);
|
|
8839
|
+
return;
|
|
8840
|
+
}
|
|
8841
|
+
const state = { started: false };
|
|
8842
|
+
try {
|
|
8843
|
+
yield* answer(client, params, true, state);
|
|
8844
|
+
} catch (err) {
|
|
8845
|
+
if (state.started || err?.status !== 400) throw err;
|
|
8846
|
+
yield* answer(client, params, false);
|
|
8847
|
+
plainModels.add(key);
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
async function* answer(client, params, tuned, state = { started: false }) {
|
|
8851
|
+
const stream = client.messages.stream(
|
|
8852
|
+
tuned ? {
|
|
8853
|
+
model: params.model,
|
|
8854
|
+
max_tokens: 4096,
|
|
8855
|
+
// The system prompt carries the retrieved context — the bulk of the
|
|
8856
|
+
// request, identical across a conversation's turns — so cache it.
|
|
8857
|
+
system: [
|
|
8858
|
+
{ type: "text", text: params.system, cache_control: { type: "ephemeral" } }
|
|
8859
|
+
],
|
|
8860
|
+
messages: params.turns,
|
|
8861
|
+
thinking: { type: "adaptive" },
|
|
8862
|
+
output_config: { effort: "medium" }
|
|
8863
|
+
} : (
|
|
8864
|
+
// The bare request every Messages-speaking model accepts.
|
|
8865
|
+
{
|
|
8866
|
+
model: params.model,
|
|
8867
|
+
max_tokens: 4096,
|
|
8868
|
+
system: params.system,
|
|
8869
|
+
messages: params.turns
|
|
8870
|
+
}
|
|
8871
|
+
),
|
|
8872
|
+
{ signal: params.signal }
|
|
8873
|
+
);
|
|
8874
|
+
for await (const event of stream) {
|
|
8875
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
8876
|
+
state.started = true;
|
|
8877
|
+
yield event.delta.text;
|
|
8878
|
+
}
|
|
8879
|
+
}
|
|
8880
|
+
const final = await stream.finalMessage();
|
|
8881
|
+
if (final.stop_reason === "refusal") {
|
|
8882
|
+
throw new ModelRefusedError(
|
|
8883
|
+
final.stop_details?.explanation || "The model declined to answer that."
|
|
8884
|
+
);
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
function describeModelError(err) {
|
|
8888
|
+
const e = err;
|
|
8889
|
+
const detail = (e?.error?.error?.message || "").toLowerCase();
|
|
8890
|
+
const provider = config.ANTHROPIC_BASE_URL ? "The model gateway" : "Anthropic";
|
|
8891
|
+
if (detail.includes("credit balance") || detail.includes("billing")) {
|
|
8892
|
+
return "Hootie's Anthropic account is out of credit. A server admin needs to add credit in the Anthropic console (Plans & Billing).";
|
|
8893
|
+
}
|
|
8894
|
+
switch (e?.status) {
|
|
8895
|
+
case 401:
|
|
8896
|
+
case 403:
|
|
8897
|
+
return `${provider} rejected this server's API key. A server admin can replace it under Settings \u2192 General.`;
|
|
8898
|
+
case 404:
|
|
8899
|
+
return `No model named "${config.HOOTIE_MODEL}" is available on this key. A server admin can change it under Settings \u2192 General.`;
|
|
8900
|
+
case 413:
|
|
8901
|
+
return "That question plus the documents behind it are too large for one request. Try asking something narrower.";
|
|
8902
|
+
case 429:
|
|
8903
|
+
return `${provider} is rate-limiting this server right now. Try again in a moment.`;
|
|
8904
|
+
case 529:
|
|
8905
|
+
return "The model is overloaded right now. Try again in a moment.";
|
|
8906
|
+
}
|
|
8907
|
+
if (typeof e?.status === "number" && e.status >= 500) {
|
|
8908
|
+
return `${provider} is having trouble right now. Try again in a moment.`;
|
|
8909
|
+
}
|
|
8910
|
+
return "Hootie couldn't reach the model. Try again in a moment.";
|
|
8911
|
+
}
|
|
8912
|
+
async function verifyModelKey(apiKey, baseUrl) {
|
|
8913
|
+
try {
|
|
8914
|
+
await clientFor(apiKey, baseUrl).models.list(
|
|
8915
|
+
{ limit: 1 },
|
|
8916
|
+
{ signal: AbortSignal.timeout(1e4) }
|
|
8917
|
+
);
|
|
8918
|
+
return null;
|
|
8919
|
+
} catch (err) {
|
|
8920
|
+
const status = err?.status;
|
|
8921
|
+
if (status === 401 || status === 403) {
|
|
8922
|
+
return baseUrl ? `The model gateway rejected that key (${status}). Check the key and the gateway URL.` : "Anthropic rejected that key. Check it and paste it again.";
|
|
8923
|
+
}
|
|
8924
|
+
console.warn("[hootie] key check inconclusive, saving anyway", status ?? err?.message);
|
|
8925
|
+
return null;
|
|
8926
|
+
}
|
|
8927
|
+
}
|
|
8928
|
+
var cached = null;
|
|
8929
|
+
function clientFor(apiKey, baseUrl) {
|
|
8930
|
+
if (!cached || cached.apiKey !== apiKey || cached.baseUrl !== baseUrl) {
|
|
8931
|
+
cached = {
|
|
8932
|
+
apiKey,
|
|
8933
|
+
baseUrl,
|
|
8934
|
+
client: new Anthropic({ apiKey, ...baseUrl ? { baseURL: baseUrl } : {} })
|
|
8935
|
+
};
|
|
8936
|
+
}
|
|
8937
|
+
return cached.client;
|
|
8938
|
+
}
|
|
8939
|
+
var ModelRefusedError = class extends Error {
|
|
8940
|
+
constructor(message) {
|
|
8941
|
+
super(message);
|
|
8942
|
+
this.name = "ModelRefusedError";
|
|
8943
|
+
}
|
|
8944
|
+
};
|
|
8945
|
+
|
|
8946
|
+
// src/hootie/routes.ts
|
|
8947
|
+
var hootieRoutes = new Hono15();
|
|
8948
|
+
var MAX_MESSAGE_CHARS = 4e3;
|
|
8949
|
+
var MAX_HISTORY_TURNS = 20;
|
|
8950
|
+
var MAX_HISTORY_CHARS = 8e3;
|
|
8951
|
+
var MAX_HISTORY_RAW = MAX_HISTORY_TURNS * 4;
|
|
8952
|
+
var CONTEXT_BUDGET = 6e3;
|
|
8953
|
+
var CHAT_BUDGET = { max: 30, windowMs: 5 * 6e4 };
|
|
8954
|
+
hootieRoutes.post("/chat", async (c) => {
|
|
8955
|
+
if (!config.HOOTIE_ENABLED) return c.json({ error: "Not found" }, 404);
|
|
8956
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
8957
|
+
if (!apiKey) {
|
|
8958
|
+
return c.json(
|
|
8959
|
+
{
|
|
8960
|
+
error: "Hootie needs an Anthropic API key. A server admin can add one under Settings \u2192 General.",
|
|
8961
|
+
reason: "no_api_key"
|
|
8962
|
+
},
|
|
8963
|
+
503
|
|
8964
|
+
);
|
|
8965
|
+
}
|
|
8966
|
+
let body;
|
|
8967
|
+
try {
|
|
8968
|
+
body = await c.req.json();
|
|
8969
|
+
} catch {
|
|
8970
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
8971
|
+
}
|
|
8972
|
+
const message = typeof body.message === "string" ? body.message.trim() : "";
|
|
8973
|
+
if (!message) return c.json({ error: "message is required" }, 400);
|
|
8974
|
+
if (message.length > MAX_MESSAGE_CHARS) {
|
|
8975
|
+
return c.json(
|
|
8976
|
+
{ error: `message is too long (max ${MAX_MESSAGE_CHARS} characters)` },
|
|
8977
|
+
400
|
|
8978
|
+
);
|
|
8979
|
+
}
|
|
8980
|
+
const nodeId = typeof body.node_id === "string" && body.node_id.trim() ? body.node_id.trim() : null;
|
|
8981
|
+
if (nodeId !== null && !isNodeIdShaped(nodeId)) {
|
|
8982
|
+
return c.json({ error: "Node not found" }, 404);
|
|
8983
|
+
}
|
|
8984
|
+
const history = normalizeHistory(body.history);
|
|
8985
|
+
if (history === null) {
|
|
8986
|
+
return c.json(
|
|
8987
|
+
{ error: "history must be a list of {role: user|assistant, content}" },
|
|
8988
|
+
400
|
|
8989
|
+
);
|
|
8990
|
+
}
|
|
8991
|
+
const nestId = c.req.param("nestId");
|
|
8992
|
+
const userId = c.get("userId");
|
|
8993
|
+
if (!tryConsume(`hootie:${userId}`, CHAT_BUDGET)) {
|
|
8994
|
+
return c.json(
|
|
8995
|
+
{ error: "Too many questions in a short time \u2014 give it a few minutes." },
|
|
8996
|
+
429
|
|
8997
|
+
);
|
|
8998
|
+
}
|
|
8999
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
9000
|
+
const baseUrl = requestBaseUrl(c.req.url);
|
|
9001
|
+
const nodes = [];
|
|
9002
|
+
const blocks = [];
|
|
9003
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9004
|
+
const take = (r) => {
|
|
9005
|
+
for (let i = 0; i < r.nodes.length; i++) {
|
|
9006
|
+
const n = r.nodes[i];
|
|
9007
|
+
if (seen.has(n.id)) continue;
|
|
9008
|
+
seen.add(n.id);
|
|
9009
|
+
nodes.push(n);
|
|
9010
|
+
blocks.push(r.blocks[i].markdown);
|
|
9011
|
+
}
|
|
9012
|
+
};
|
|
9013
|
+
let anchor = null;
|
|
9014
|
+
if (nodeId) {
|
|
9015
|
+
const r = await resolveContext({
|
|
9016
|
+
nestId,
|
|
9017
|
+
userId,
|
|
9018
|
+
baseUrl,
|
|
9019
|
+
body: {
|
|
9020
|
+
selector: `[[${nodeId}]]`,
|
|
9021
|
+
hops: 1,
|
|
9022
|
+
max_tokens: CONTEXT_BUDGET,
|
|
9023
|
+
include_drafts: true
|
|
9024
|
+
}
|
|
9025
|
+
});
|
|
9026
|
+
anchor = r.nodes.find((n) => n.id === nodeId) ?? null;
|
|
9027
|
+
if (!anchor) return c.json({ error: "Node not found" }, 404);
|
|
9028
|
+
take(r);
|
|
9029
|
+
}
|
|
9030
|
+
const question = stripMentions(message);
|
|
9031
|
+
if (question) {
|
|
9032
|
+
const r = await resolveContext({
|
|
9033
|
+
nestId,
|
|
9034
|
+
userId,
|
|
9035
|
+
baseUrl,
|
|
9036
|
+
body: { prompt: question, max_tokens: CONTEXT_BUDGET }
|
|
9037
|
+
});
|
|
9038
|
+
take(r);
|
|
9039
|
+
}
|
|
9040
|
+
const nest = await getNest(nestId);
|
|
9041
|
+
const system = buildSystemPrompt({
|
|
9042
|
+
nestName: nest?.name ?? "this nest",
|
|
9043
|
+
userEmail,
|
|
9044
|
+
anchor,
|
|
9045
|
+
blocks
|
|
9046
|
+
});
|
|
9047
|
+
const turns = [...history, { role: "user", content: message }];
|
|
9048
|
+
const mentionNames = extractMentions(message);
|
|
9049
|
+
trackEvent("hootie.chat", {
|
|
9050
|
+
nestId,
|
|
9051
|
+
anchored: !!anchor,
|
|
9052
|
+
context_nodes: nodes.length,
|
|
9053
|
+
history_turns: history.length,
|
|
9054
|
+
mentions: mentionNames.length
|
|
9055
|
+
});
|
|
9056
|
+
return streamSSE(c, async (stream) => {
|
|
9057
|
+
const cancel = new AbortController();
|
|
9058
|
+
const hangUp = () => cancel.abort();
|
|
9059
|
+
stream.onAbort(hangUp);
|
|
9060
|
+
c.req.raw.signal.addEventListener("abort", hangUp, { once: true });
|
|
9061
|
+
await stream.writeSSE({
|
|
9062
|
+
event: "meta",
|
|
9063
|
+
data: JSON.stringify({ nodes, anchor })
|
|
9064
|
+
});
|
|
9065
|
+
let answer2 = "";
|
|
9066
|
+
try {
|
|
9067
|
+
for await (const chunk of streamAnswer({
|
|
9068
|
+
apiKey,
|
|
9069
|
+
model: config.HOOTIE_MODEL,
|
|
9070
|
+
baseUrl: config.ANTHROPIC_BASE_URL || null,
|
|
9071
|
+
system,
|
|
9072
|
+
turns,
|
|
9073
|
+
signal: cancel.signal
|
|
9074
|
+
})) {
|
|
9075
|
+
if (cancel.signal.aborted) return;
|
|
9076
|
+
answer2 += chunk;
|
|
9077
|
+
await stream.writeSSE({ event: "delta", data: JSON.stringify({ text: chunk }) });
|
|
9078
|
+
}
|
|
9079
|
+
} catch (err) {
|
|
9080
|
+
if (cancel.signal.aborted) return;
|
|
9081
|
+
const text = err instanceof ModelRefusedError ? err.message : describeModelError(err);
|
|
9082
|
+
console.error("[hootie] model call failed", nestId, err);
|
|
9083
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: text }) });
|
|
9084
|
+
return;
|
|
9085
|
+
}
|
|
9086
|
+
if (cancel.signal.aborted) return;
|
|
9087
|
+
const cited = citedNodes(answer2, nodes);
|
|
9088
|
+
let mention = null;
|
|
9089
|
+
if (mentionNames.length > 0) {
|
|
9090
|
+
mention = await deliverMentions({
|
|
9091
|
+
nestId,
|
|
9092
|
+
userEmail,
|
|
9093
|
+
baseUrl,
|
|
9094
|
+
message,
|
|
9095
|
+
answer: answer2,
|
|
9096
|
+
anchor: anchor ?? cited[0] ?? null
|
|
9097
|
+
});
|
|
9098
|
+
}
|
|
9099
|
+
await stream.writeSSE({
|
|
9100
|
+
event: "done",
|
|
9101
|
+
data: JSON.stringify({ answer: answer2, cited, mention })
|
|
9102
|
+
});
|
|
9103
|
+
});
|
|
9104
|
+
});
|
|
9105
|
+
var NODE_ID_SHAPE = /^nodes(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/;
|
|
9106
|
+
function isNodeIdShaped(id) {
|
|
9107
|
+
return id.length <= 512 && NODE_ID_SHAPE.test(id);
|
|
9108
|
+
}
|
|
9109
|
+
function normalizeHistory(raw) {
|
|
9110
|
+
if (raw === void 0 || raw === null) return [];
|
|
9111
|
+
if (!Array.isArray(raw)) return null;
|
|
9112
|
+
const tail = raw.length > MAX_HISTORY_RAW ? raw.slice(-MAX_HISTORY_RAW) : raw;
|
|
9113
|
+
const turns = [];
|
|
9114
|
+
for (const t of tail) {
|
|
9115
|
+
if (!t || typeof t !== "object") return null;
|
|
9116
|
+
const role = t.role;
|
|
9117
|
+
const content = t.content;
|
|
9118
|
+
if (role !== "user" && role !== "assistant" || typeof content !== "string")
|
|
9119
|
+
return null;
|
|
9120
|
+
const text = content.trim();
|
|
9121
|
+
if (!text) continue;
|
|
9122
|
+
turns.push({ role, content: text });
|
|
9123
|
+
}
|
|
9124
|
+
const merged = [];
|
|
9125
|
+
for (const t of turns) {
|
|
9126
|
+
const last = merged[merged.length - 1];
|
|
9127
|
+
if (last && last.role === t.role) last.content += "\n\n" + t.content;
|
|
9128
|
+
else merged.push({ ...t });
|
|
9129
|
+
}
|
|
9130
|
+
let kept = merged.slice(-MAX_HISTORY_TURNS);
|
|
9131
|
+
let chars = kept.reduce((n, t) => n + t.content.length, 0);
|
|
9132
|
+
while (kept.length > 0 && chars > MAX_HISTORY_CHARS) {
|
|
9133
|
+
chars -= kept[0].content.length;
|
|
9134
|
+
kept = kept.slice(1);
|
|
9135
|
+
}
|
|
9136
|
+
while (kept.length > 0 && kept[0].role !== "user") kept = kept.slice(1);
|
|
9137
|
+
while (kept.length > 0 && kept[kept.length - 1].role !== "assistant")
|
|
9138
|
+
kept = kept.slice(0, -1);
|
|
9139
|
+
return kept;
|
|
9140
|
+
}
|
|
9141
|
+
function buildSystemPrompt(params) {
|
|
9142
|
+
const { nestName: nestName2, userEmail, anchor, blocks } = params;
|
|
9143
|
+
const context = blocks.length > 0 ? blocks.join("\n\n---\n\n").replace(/<\/documents>/gi, "</ documents>") : "(nothing in the nest matched this question)";
|
|
9144
|
+
return [
|
|
9145
|
+
`You are Hootie, the assistant inside the ContextNest nest "${nestName2}". You are talking with ${userEmail}.`,
|
|
9146
|
+
"",
|
|
9147
|
+
"Answer from the documents below and nothing else. They are the governed context this person is allowed to read; do not invent facts that are not in them.",
|
|
9148
|
+
"When you use a document, cite it by its exact title as a wiki link, like [[Title]] \u2014 this is how the reader opens it. Cite every document you drew on, and only those.",
|
|
9149
|
+
"If the documents do not answer the question, say so plainly and, if you can, point to which document comes closest or what to search for. Do not guess.",
|
|
9150
|
+
"Be concise and direct. Use short paragraphs or a short list; no preamble.",
|
|
9151
|
+
"Names written as @someone in the message are colleagues the person is looping in \u2014 they are not questions for you, and you should not address them.",
|
|
9152
|
+
anchor ? `The person currently has the document [[${anchor.title}]] open; "this" in their message means that document unless they say otherwise.` : "The person is looking at the nest as a whole, not a particular document.",
|
|
9153
|
+
"",
|
|
9154
|
+
"<documents>",
|
|
9155
|
+
context,
|
|
9156
|
+
"</documents>"
|
|
9157
|
+
].join("\n");
|
|
9158
|
+
}
|
|
9159
|
+
function citedNodes(answer2, nodes) {
|
|
9160
|
+
const lower = answer2.toLowerCase();
|
|
9161
|
+
const seenTitles = /* @__PURE__ */ new Set();
|
|
9162
|
+
const hits = [];
|
|
9163
|
+
for (const n of nodes) {
|
|
9164
|
+
const key = n.title.toLowerCase();
|
|
9165
|
+
if (seenTitles.has(key)) continue;
|
|
9166
|
+
const at = lower.indexOf(`[[${key}]]`);
|
|
9167
|
+
if (at < 0) continue;
|
|
9168
|
+
seenTitles.add(key);
|
|
9169
|
+
hits.push({ n, at });
|
|
9170
|
+
}
|
|
9171
|
+
return hits.sort((a, b) => a.at - b.at).map((x) => x.n);
|
|
9172
|
+
}
|
|
9173
|
+
async function deliverMentions(params) {
|
|
9174
|
+
const { nestId, userEmail, baseUrl, message, answer: answer2, anchor } = params;
|
|
9175
|
+
if (!anchor) return { status: "undeliverable", reason: "no_anchor" };
|
|
9176
|
+
const body = [
|
|
9177
|
+
`**Asked Hootie:** ${message}`,
|
|
9178
|
+
"",
|
|
9179
|
+
"**Hootie answered:**",
|
|
9180
|
+
answer2.trim()
|
|
9181
|
+
].join("\n");
|
|
9182
|
+
let threadId;
|
|
9183
|
+
try {
|
|
9184
|
+
const thread = await createThread(
|
|
9185
|
+
nestId,
|
|
9186
|
+
anchor.id,
|
|
9187
|
+
{ body, anchor: null, snapshotVersion: null },
|
|
9188
|
+
userEmail
|
|
9189
|
+
);
|
|
9190
|
+
threadId = thread.id;
|
|
9191
|
+
} catch (err) {
|
|
9192
|
+
console.error("[hootie] mention comment failed", nestId, anchor.id, err);
|
|
9193
|
+
return { status: "undeliverable", reason: "post_failed" };
|
|
9194
|
+
}
|
|
9195
|
+
const notified = await notifyCommentMentions({
|
|
9196
|
+
nestId,
|
|
9197
|
+
nodeId: anchor.id,
|
|
9198
|
+
body: message,
|
|
9199
|
+
actorEmail: userEmail,
|
|
9200
|
+
baseUrl
|
|
9201
|
+
});
|
|
9202
|
+
await syncAnnotationsNode(nestId, anchor.id, userEmail);
|
|
9203
|
+
return {
|
|
9204
|
+
status: "posted",
|
|
9205
|
+
node_id: anchor.id,
|
|
9206
|
+
node_title: anchor.title,
|
|
9207
|
+
thread_id: threadId,
|
|
9208
|
+
notified
|
|
9209
|
+
};
|
|
9210
|
+
}
|
|
9211
|
+
|
|
8757
9212
|
// src/mcp/routes.ts
|
|
8758
|
-
import { Hono as
|
|
9213
|
+
import { Hono as Hono17 } from "hono";
|
|
8759
9214
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8760
9215
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
8761
9216
|
|
|
@@ -9164,7 +9619,7 @@ async function listRuns(nestId, opts) {
|
|
|
9164
9619
|
}
|
|
9165
9620
|
|
|
9166
9621
|
// src/workflow/edge-routes.ts
|
|
9167
|
-
import { Hono as
|
|
9622
|
+
import { Hono as Hono16 } from "hono";
|
|
9168
9623
|
import { v4 as uuid10 } from "uuid";
|
|
9169
9624
|
function toResponse(e, t) {
|
|
9170
9625
|
return {
|
|
@@ -9283,7 +9738,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
9283
9738
|
);
|
|
9284
9739
|
}
|
|
9285
9740
|
}
|
|
9286
|
-
var edgeRoutes = new
|
|
9741
|
+
var edgeRoutes = new Hono16();
|
|
9287
9742
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
9288
9743
|
const nestId = c.req.param("nestId");
|
|
9289
9744
|
const db = getDb();
|
|
@@ -10434,7 +10889,7 @@ async function resolveLlmBody(ctx, node, gate) {
|
|
|
10434
10889
|
const approved = gate ? gate.approvedVersions?.get(node.id) ?? null : await getApprovedVersion(ctx.nestId, node.id);
|
|
10435
10890
|
if (approved == null) return null;
|
|
10436
10891
|
try {
|
|
10437
|
-
return await ctx.versionManager.
|
|
10892
|
+
return (await reconstructApproved(ctx.versionManager, ctx.nestId, node.id, approved)).raw;
|
|
10438
10893
|
} catch (err) {
|
|
10439
10894
|
console.error("reconstructVersion failed", node.id, approved, err);
|
|
10440
10895
|
return null;
|
|
@@ -11470,11 +11925,11 @@ ${list}`,
|
|
|
11470
11925
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
11471
11926
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
11472
11927
|
try {
|
|
11473
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
11928
|
+
const { createGrant: createGrant2 } = await import("./grants-service-WICV4Y2J.js");
|
|
11474
11929
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
11475
11930
|
const e = normalizeEmail2(String(args.email || ""));
|
|
11476
11931
|
if (!e) return "email is required.";
|
|
11477
|
-
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-
|
|
11932
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-XEBX6BM4.js");
|
|
11478
11933
|
const row = await findOrCreateInvitedUser2(e, { invitedBy: ctx.userEmail });
|
|
11479
11934
|
await createGrant2({
|
|
11480
11935
|
nestId: ctx.nestId,
|
|
@@ -11528,7 +11983,7 @@ ${list}`;
|
|
|
11528
11983
|
|
|
11529
11984
|
// src/mcp/routes.ts
|
|
11530
11985
|
import { z as z2 } from "zod";
|
|
11531
|
-
var mcpRoutes = new
|
|
11986
|
+
var mcpRoutes = new Hono17();
|
|
11532
11987
|
async function getUserEmail2(userId) {
|
|
11533
11988
|
const db = getDb();
|
|
11534
11989
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
@@ -11598,7 +12053,7 @@ mcpRoutes.all("/", async (c) => {
|
|
|
11598
12053
|
});
|
|
11599
12054
|
|
|
11600
12055
|
// src/workflow/run-routes.ts
|
|
11601
|
-
import { Hono as
|
|
12056
|
+
import { Hono as Hono18 } from "hono";
|
|
11602
12057
|
import { v4 as uuid12 } from "uuid";
|
|
11603
12058
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
11604
12059
|
var safeJson2 = (s) => {
|
|
@@ -11717,7 +12172,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
11717
12172
|
definitions
|
|
11718
12173
|
};
|
|
11719
12174
|
}
|
|
11720
|
-
var runTriggerRoutes = new
|
|
12175
|
+
var runTriggerRoutes = new Hono18();
|
|
11721
12176
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
11722
12177
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
11723
12178
|
throw new ValidationError(
|
|
@@ -11825,7 +12280,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
11825
12280
|
201
|
|
11826
12281
|
);
|
|
11827
12282
|
});
|
|
11828
|
-
var runListRoutes = new
|
|
12283
|
+
var runListRoutes = new Hono18();
|
|
11829
12284
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11830
12285
|
const nestId = c.req.param("nestId");
|
|
11831
12286
|
const db = getDb();
|
|
@@ -11857,7 +12312,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
11857
12312
|
}))
|
|
11858
12313
|
});
|
|
11859
12314
|
});
|
|
11860
|
-
var runDetailRoutes = new
|
|
12315
|
+
var runDetailRoutes = new Hono18();
|
|
11861
12316
|
async function loadRunWithPermission(c, runId, required) {
|
|
11862
12317
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
11863
12318
|
runId
|
|
@@ -12008,7 +12463,7 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
12008
12463
|
});
|
|
12009
12464
|
|
|
12010
12465
|
// src/workflow/schedule-routes.ts
|
|
12011
|
-
import { Hono as
|
|
12466
|
+
import { Hono as Hono19 } from "hono";
|
|
12012
12467
|
import { v4 as uuid13 } from "uuid";
|
|
12013
12468
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
12014
12469
|
var MIN_EVERY = 5;
|
|
@@ -12046,7 +12501,7 @@ function parseEvery(v) {
|
|
|
12046
12501
|
}
|
|
12047
12502
|
return n;
|
|
12048
12503
|
}
|
|
12049
|
-
var scheduleRoutes = new
|
|
12504
|
+
var scheduleRoutes = new Hono19();
|
|
12050
12505
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
12051
12506
|
const nestId = c.req.param("nestId");
|
|
12052
12507
|
const rows = await getDb().all(
|
|
@@ -12142,7 +12597,7 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
12142
12597
|
});
|
|
12143
12598
|
|
|
12144
12599
|
// src/notify/connector-routes.ts
|
|
12145
|
-
import { Hono as
|
|
12600
|
+
import { Hono as Hono20 } from "hono";
|
|
12146
12601
|
import { v4 as uuid14 } from "uuid";
|
|
12147
12602
|
var KNOWN_EVENTS = [
|
|
12148
12603
|
"review_requested",
|
|
@@ -12190,7 +12645,7 @@ function parseUrl(v) {
|
|
|
12190
12645
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
12191
12646
|
);
|
|
12192
12647
|
}
|
|
12193
|
-
var connectorRoutes = new
|
|
12648
|
+
var connectorRoutes = new Hono20();
|
|
12194
12649
|
connectorRoutes.get("/", async (c) => {
|
|
12195
12650
|
const nestId = c.req.param("nestId");
|
|
12196
12651
|
const rows = await getDb().all(
|
|
@@ -12252,7 +12707,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
12252
12707
|
});
|
|
12253
12708
|
|
|
12254
12709
|
// src/workflow/hook-routes.ts
|
|
12255
|
-
import { Hono as
|
|
12710
|
+
import { Hono as Hono21 } from "hono";
|
|
12256
12711
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
12257
12712
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
12258
12713
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -12275,7 +12730,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
12275
12730
|
created_at: r.created_at
|
|
12276
12731
|
};
|
|
12277
12732
|
}
|
|
12278
|
-
var hookNestRoutes = new
|
|
12733
|
+
var hookNestRoutes = new Hono21();
|
|
12279
12734
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
12280
12735
|
const nestId = c.req.param("nestId");
|
|
12281
12736
|
const agent = c.req.query("agent");
|
|
@@ -12346,10 +12801,10 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
12346
12801
|
const mine = createHmac2("sha256", Buffer.from(secret, "base64")).update(rawBody, "utf8").digest("base64");
|
|
12347
12802
|
return safeEqual(mine, authHeader.slice(5).trim());
|
|
12348
12803
|
}
|
|
12349
|
-
function
|
|
12804
|
+
function stripMentions2(text) {
|
|
12350
12805
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
12351
12806
|
}
|
|
12352
|
-
var hookFireRoutes = new
|
|
12807
|
+
var hookFireRoutes = new Hono21();
|
|
12353
12808
|
hookFireRoutes.post("/:token", async (c) => {
|
|
12354
12809
|
const token = c.req.param("token");
|
|
12355
12810
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -12397,7 +12852,7 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
12397
12852
|
if (payload.type === "event_callback") {
|
|
12398
12853
|
const ev = payload.event ?? {};
|
|
12399
12854
|
if (ev.type !== "app_mention") return c.json({ ok: true });
|
|
12400
|
-
inputs = { input:
|
|
12855
|
+
inputs = { input: stripMentions2(String(ev.text ?? "")), from: ev.user, channel: ev.channel };
|
|
12401
12856
|
ack = () => c.json({ ok: true });
|
|
12402
12857
|
} else {
|
|
12403
12858
|
inputs = {
|
|
@@ -12415,7 +12870,7 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
12415
12870
|
if (secret && !teamsSignatureValid(secret, c.req.header("authorization"), raw)) {
|
|
12416
12871
|
return c.json({ error: "bad signature" }, 401);
|
|
12417
12872
|
}
|
|
12418
|
-
inputs = { input:
|
|
12873
|
+
inputs = { input: stripMentions2(String(payload.text ?? "")), from: payload.from?.name };
|
|
12419
12874
|
ack = (runId2) => c.json({ type: "message", text: `\u23F3 Queued ${hook.agent_node} (run ${runId2.slice(0, 12)}\u2026)` });
|
|
12420
12875
|
} else {
|
|
12421
12876
|
inputs = Object.keys(payload).length ? payload : null;
|
|
@@ -12451,9 +12906,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
12451
12906
|
});
|
|
12452
12907
|
|
|
12453
12908
|
// src/definitions/routes.ts
|
|
12454
|
-
import { Hono as
|
|
12909
|
+
import { Hono as Hono22 } from "hono";
|
|
12455
12910
|
import { v4 as uuid15 } from "uuid";
|
|
12456
|
-
var definitionRoutes = new
|
|
12911
|
+
var definitionRoutes = new Hono22();
|
|
12457
12912
|
definitionRoutes.get("/", async (c) => {
|
|
12458
12913
|
const nestId = c.req.param("nestId");
|
|
12459
12914
|
const db = getDb();
|
|
@@ -12561,11 +13016,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
12561
13016
|
});
|
|
12562
13017
|
|
|
12563
13018
|
// src/nests/asset-routes.ts
|
|
12564
|
-
import { Hono as
|
|
13019
|
+
import { Hono as Hono23 } from "hono";
|
|
12565
13020
|
import { v4 as uuid16 } from "uuid";
|
|
12566
13021
|
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
12567
13022
|
import { join as join6 } from "path";
|
|
12568
|
-
var assetRoutes = new
|
|
13023
|
+
var assetRoutes = new Hono23();
|
|
12569
13024
|
var CONTENT_TYPES = {
|
|
12570
13025
|
png: "image/png",
|
|
12571
13026
|
jpg: "image/jpeg",
|
|
@@ -12652,19 +13107,21 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
12652
13107
|
});
|
|
12653
13108
|
|
|
12654
13109
|
// src/nodes/pdf-routes.ts
|
|
12655
|
-
import { Hono as
|
|
13110
|
+
import { Hono as Hono24 } from "hono";
|
|
12656
13111
|
import { bodyLimit } from "hono/body-limit";
|
|
12657
13112
|
import { createHash as createHash3 } from "crypto";
|
|
12658
13113
|
import { isPdf, readPdfBinary as readPdfBinary3 } from "@promptowl/contextnest-engine";
|
|
12659
|
-
var pdfNodeRoutes = new
|
|
13114
|
+
var pdfNodeRoutes = new Hono24();
|
|
12660
13115
|
var PDF_UPLOAD_PATH = /^\/nests\/[^/]+\/nodes\/pdf\/?$/;
|
|
12661
13116
|
function pdfMaxRequestBytes() {
|
|
12662
13117
|
return config.PDF_MAX_BYTES + 1024 * 1024;
|
|
12663
13118
|
}
|
|
13119
|
+
function pdfTooLargeMessage(bytes) {
|
|
13120
|
+
const size = bytes === void 0 ? "" : `${(bytes / (1024 * 1024)).toFixed(1)} MB, `;
|
|
13121
|
+
return `PDF too large (${size}max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`;
|
|
13122
|
+
}
|
|
12664
13123
|
function tooLarge(bytes) {
|
|
12665
|
-
return new PayloadTooLargeError(
|
|
12666
|
-
`PDF too large (${(bytes / (1024 * 1024)).toFixed(1)} MB, max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12667
|
-
);
|
|
13124
|
+
return new PayloadTooLargeError(pdfTooLargeMessage(bytes));
|
|
12668
13125
|
}
|
|
12669
13126
|
function field(body, name) {
|
|
12670
13127
|
const v = body[name];
|
|
@@ -12685,12 +13142,7 @@ pdfNodeRoutes.post(
|
|
|
12685
13142
|
"/pdf",
|
|
12686
13143
|
(c, next) => bodyLimit({
|
|
12687
13144
|
maxSize: pdfMaxRequestBytes(),
|
|
12688
|
-
onError: (c2) => c2.json(
|
|
12689
|
-
{
|
|
12690
|
-
error: `PDF too large (max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12691
|
-
},
|
|
12692
|
-
413
|
|
12693
|
-
)
|
|
13145
|
+
onError: (c2) => c2.json({ error: pdfTooLargeMessage() }, 413)
|
|
12694
13146
|
})(c, next),
|
|
12695
13147
|
async (c) => {
|
|
12696
13148
|
const nestId = c.req.param("nestId");
|
|
@@ -12829,7 +13281,7 @@ pdfNodeRoutes.get("/:nodeId{.+}/pdf", async (c, next) => {
|
|
|
12829
13281
|
});
|
|
12830
13282
|
|
|
12831
13283
|
// src/auth/community-sites-routes.ts
|
|
12832
|
-
import { Hono as
|
|
13284
|
+
import { Hono as Hono25 } from "hono";
|
|
12833
13285
|
var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
12834
13286
|
var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
|
|
12835
13287
|
var serializeSite = (site) => ({
|
|
@@ -12841,7 +13293,7 @@ var serializeSite = (site) => ({
|
|
|
12841
13293
|
created_at: site.created_at,
|
|
12842
13294
|
updated_at: site.updated_at
|
|
12843
13295
|
});
|
|
12844
|
-
var communitySiteRoutes = new
|
|
13296
|
+
var communitySiteRoutes = new Hono25();
|
|
12845
13297
|
communitySiteRoutes.get("/", async (c) => {
|
|
12846
13298
|
if (!await serverAdminAllowed(c))
|
|
12847
13299
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
@@ -12929,7 +13381,7 @@ communitySiteRoutes.delete("/:id", async (c) => {
|
|
|
12929
13381
|
});
|
|
12930
13382
|
|
|
12931
13383
|
// src/mcp/server-routes.ts
|
|
12932
|
-
import { Hono as
|
|
13384
|
+
import { Hono as Hono26 } from "hono";
|
|
12933
13385
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12934
13386
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
12935
13387
|
import { z as z3 } from "zod";
|
|
@@ -13265,7 +13717,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
|
|
|
13265
13717
|
}
|
|
13266
13718
|
return server;
|
|
13267
13719
|
}
|
|
13268
|
-
var serverMcpRoutes = new
|
|
13720
|
+
var serverMcpRoutes = new Hono26();
|
|
13269
13721
|
serverMcpRoutes.all("/", async (c) => {
|
|
13270
13722
|
const userId = c.get("userId");
|
|
13271
13723
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -13288,7 +13740,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
13288
13740
|
await server.server.close();
|
|
13289
13741
|
}
|
|
13290
13742
|
});
|
|
13291
|
-
var nestIndexRoutes = new
|
|
13743
|
+
var nestIndexRoutes = new Hono26();
|
|
13292
13744
|
nestIndexRoutes.get("/", async (c) => {
|
|
13293
13745
|
const userId = c.get("userId");
|
|
13294
13746
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -13464,14 +13916,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
|
13464
13916
|
through steward review before they become AI-visible.
|
|
13465
13917
|
`;
|
|
13466
13918
|
}
|
|
13467
|
-
var llmsTxtRoutes = new
|
|
13919
|
+
var llmsTxtRoutes = new Hono26();
|
|
13468
13920
|
llmsTxtRoutes.get("/", (c) => {
|
|
13469
13921
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
13470
13922
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
13471
13923
|
});
|
|
13472
13924
|
|
|
13473
13925
|
// src/governance/routes.ts
|
|
13474
|
-
import { Hono as
|
|
13926
|
+
import { Hono as Hono27 } from "hono";
|
|
13475
13927
|
|
|
13476
13928
|
// src/governance/feed-service.ts
|
|
13477
13929
|
var SNIPPET_CHARS = 200;
|
|
@@ -13903,7 +14355,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
13903
14355
|
if (Number.isNaN(n) || n < 1) return def;
|
|
13904
14356
|
return Math.min(n, max);
|
|
13905
14357
|
}
|
|
13906
|
-
var governanceRoutes = new
|
|
14358
|
+
var governanceRoutes = new Hono27();
|
|
13907
14359
|
governanceRoutes.get("/stewards", async (c) => {
|
|
13908
14360
|
const nestId = c.req.param("nestId");
|
|
13909
14361
|
const scope = c.req.query("scope");
|
|
@@ -14273,7 +14725,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
14273
14725
|
const activity = await getActivity({ nestId, limit });
|
|
14274
14726
|
return c.json({ activity });
|
|
14275
14727
|
});
|
|
14276
|
-
var governanceNodeRoutes = new
|
|
14728
|
+
var governanceNodeRoutes = new Hono27();
|
|
14277
14729
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
14278
14730
|
const nestId = c.req.param("nestId");
|
|
14279
14731
|
const nodeId = c.req.param("nodeId");
|
|
@@ -14905,7 +15357,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
14905
15357
|
return ok;
|
|
14906
15358
|
}
|
|
14907
15359
|
function createApp() {
|
|
14908
|
-
const app = new
|
|
15360
|
+
const app = new Hono28({ router: new LinearRouter() });
|
|
14909
15361
|
const corsOrigins = config.CORS_ORIGINS;
|
|
14910
15362
|
app.use(
|
|
14911
15363
|
"*",
|
|
@@ -14924,11 +15376,13 @@ function createApp() {
|
|
|
14924
15376
|
if (lenStr) {
|
|
14925
15377
|
const len = parseInt(lenStr, 10);
|
|
14926
15378
|
const isPost = c.req.method === "POST";
|
|
14927
|
-
const
|
|
15379
|
+
const isPdf2 = isPost && PDF_UPLOAD_PATH.test(c.req.path);
|
|
15380
|
+
const cap = isPost && ASSET_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, ASSET_MAX_REQUEST_BYTES) : isPdf2 ? Math.max(config.MAX_BODY_BYTES, pdfMaxRequestBytes()) : config.MAX_BODY_BYTES;
|
|
14928
15381
|
if (Number.isFinite(len) && len > cap) {
|
|
14929
15382
|
return c.json(
|
|
14930
15383
|
{
|
|
14931
|
-
|
|
15384
|
+
// A PDF gets the same message as the route's own gates.
|
|
15385
|
+
error: isPdf2 ? pdfTooLargeMessage(len) : `Request body too large (${len} bytes, max ${cap})`
|
|
14932
15386
|
},
|
|
14933
15387
|
413
|
|
14934
15388
|
);
|
|
@@ -15001,6 +15455,11 @@ function createApp() {
|
|
|
15001
15455
|
// API would refuse it — existing nodes of the type stay readable.
|
|
15002
15456
|
type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
|
|
15003
15457
|
type_table_enabled: config.TYPE_TABLE_ENABLED,
|
|
15458
|
+
// Hootie in the nest (default ON). Off → POST /nests/:id/chat 404s and
|
|
15459
|
+
// the UI hides the launcher. Whether it can actually answer is
|
|
15460
|
+
// runner_key_configured above — the UI shows the launcher whenever the
|
|
15461
|
+
// feature is on and explains the missing key, rather than hiding it.
|
|
15462
|
+
hootie_enabled: config.HOOTIE_ENABLED,
|
|
15004
15463
|
// This server hosts several organizations (PromptOwl-hosted) rather than
|
|
15005
15464
|
// one. The visibility picker words 'Organization' and 'Public' by it and
|
|
15006
15465
|
// confirms before 'org'; the server enforces the same in
|
|
@@ -15115,6 +15574,9 @@ function createApp() {
|
|
|
15115
15574
|
promptowl_teams_enabled: config.PROMPTOWL_TEAMS_ENABLED,
|
|
15116
15575
|
type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
|
|
15117
15576
|
type_table_enabled: config.TYPE_TABLE_ENABLED,
|
|
15577
|
+
hootie_enabled: config.HOOTIE_ENABLED,
|
|
15578
|
+
hootie_model: config.HOOTIE_MODEL,
|
|
15579
|
+
anthropic_base_url: config.ANTHROPIC_BASE_URL,
|
|
15118
15580
|
subagent_runs_enabled: config.FEATURE_SUBAGENT_RUNS,
|
|
15119
15581
|
subagent_max_depth: config.SUBAGENT_MAX_DEPTH,
|
|
15120
15582
|
subagent_max_children: config.SUBAGENT_MAX_CHILDREN,
|
|
@@ -15253,6 +15715,38 @@ function createApp() {
|
|
|
15253
15715
|
value: body.type_table_enabled ? null : "false"
|
|
15254
15716
|
});
|
|
15255
15717
|
}
|
|
15718
|
+
if ("hootie_enabled" in body) {
|
|
15719
|
+
pending.push({
|
|
15720
|
+
name: "HOOTIE_ENABLED",
|
|
15721
|
+
value: body.hootie_enabled ? null : "false"
|
|
15722
|
+
});
|
|
15723
|
+
}
|
|
15724
|
+
if ("hootie_model" in body) {
|
|
15725
|
+
const v = String(body.hootie_model ?? "").trim();
|
|
15726
|
+
if (!v) addError("hootie_model", "Model is required \u2014 e.g. claude-opus-5.");
|
|
15727
|
+
else if (v.length > 200 || /\s/.test(v))
|
|
15728
|
+
addError("hootie_model", "Model must be a single identifier with no spaces.");
|
|
15729
|
+
else pending.push({ name: "HOOTIE_MODEL", value: v === "claude-opus-5" ? null : v });
|
|
15730
|
+
}
|
|
15731
|
+
if ("anthropic_base_url" in body) {
|
|
15732
|
+
const v = String(body.anthropic_base_url ?? "").trim().replace(/\/$/, "");
|
|
15733
|
+
if (!v) pending.push({ name: "ANTHROPIC_BASE_URL", value: null });
|
|
15734
|
+
else {
|
|
15735
|
+
let ok = false;
|
|
15736
|
+
try {
|
|
15737
|
+
const u = new URL(v);
|
|
15738
|
+
ok = (u.protocol === "http:" || u.protocol === "https:") && !!u.host;
|
|
15739
|
+
} catch {
|
|
15740
|
+
ok = false;
|
|
15741
|
+
}
|
|
15742
|
+
if (!ok)
|
|
15743
|
+
addError(
|
|
15744
|
+
"anthropic_base_url",
|
|
15745
|
+
"Model gateway URL must be an http(s) URL, e.g. http://gateway:4000 \u2014 or empty to call Anthropic directly."
|
|
15746
|
+
);
|
|
15747
|
+
else pending.push({ name: "ANTHROPIC_BASE_URL", value: v });
|
|
15748
|
+
}
|
|
15749
|
+
}
|
|
15256
15750
|
if ("anthropic_api_key" in body) {
|
|
15257
15751
|
const v = body.anthropic_api_key;
|
|
15258
15752
|
if (v === null || v === "") {
|
|
@@ -15260,7 +15754,12 @@ function createApp() {
|
|
|
15260
15754
|
} else if (typeof v !== "string" || !v.trim().startsWith("sk-")) {
|
|
15261
15755
|
addError("anthropic_api_key", 'Anthropic API key must start with "sk-" (or be empty to clear it).');
|
|
15262
15756
|
} else {
|
|
15263
|
-
|
|
15757
|
+
const key = v.trim();
|
|
15758
|
+
const patchedBase = pending.find((p) => p.name === "ANTHROPIC_BASE_URL");
|
|
15759
|
+
const baseUrl = patchedBase ? patchedBase.value : config.ANTHROPIC_BASE_URL || null;
|
|
15760
|
+
const rejected = await verifyModelKey(key, baseUrl);
|
|
15761
|
+
if (rejected) addError("anthropic_api_key", rejected);
|
|
15762
|
+
else pending.push({ name: "ANTHROPIC_API_KEY", value: key });
|
|
15264
15763
|
}
|
|
15265
15764
|
}
|
|
15266
15765
|
if ("subagent_runs_enabled" in body) {
|
|
@@ -15976,7 +16475,7 @@ function createApp() {
|
|
|
15976
16475
|
app.route("/index", nestIndexRoutes);
|
|
15977
16476
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
15978
16477
|
app.route("/mcp", serverMcpRoutes);
|
|
15979
|
-
const nestsApp = new
|
|
16478
|
+
const nestsApp = new Hono28();
|
|
15980
16479
|
nestsApp.use("*", async (c, next) => {
|
|
15981
16480
|
const parts = nestPathParts(c.req.path);
|
|
15982
16481
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -16055,7 +16554,7 @@ function createApp() {
|
|
|
16055
16554
|
if (access.archived && isMcpTransport) {
|
|
16056
16555
|
return deny(404, "Nest not found");
|
|
16057
16556
|
}
|
|
16058
|
-
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
|
|
16557
|
+
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export", "chat"].includes(parts[1]);
|
|
16059
16558
|
let grantRole = null;
|
|
16060
16559
|
let grantNestVisible = false;
|
|
16061
16560
|
if (permission === "none") {
|
|
@@ -16202,6 +16701,7 @@ function createApp() {
|
|
|
16202
16701
|
nestsApp.route("/:nestId/nodes", governanceNodeRoutes);
|
|
16203
16702
|
nestsApp.route("/:nestId/nodes", nodeRoutes);
|
|
16204
16703
|
nestsApp.route("/:nestId", queryRoutes);
|
|
16704
|
+
nestsApp.route("/:nestId", hootieRoutes);
|
|
16205
16705
|
nestsApp.route("/:nestId", sharingRoutes);
|
|
16206
16706
|
nestsApp.route("/:nestId", brandingRoutes);
|
|
16207
16707
|
nestsApp.route("/:nestId/grants", grantRoutes);
|