@promptowl/contextnest-community 1.23.0 → 1.24.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.
Files changed (89) hide show
  1. package/API.md +2092 -0
  2. package/CONFIGURATION.md +1 -0
  3. package/README.md +61 -9
  4. package/STEWARDSHIP.md +229 -0
  5. package/dist/{chunk-2YPY2HGZ.js → chunk-2TPQTN4Y.js} +5 -4
  6. package/dist/{chunk-MLQU4I5J.js → chunk-5EZOPA47.js} +5 -5
  7. package/dist/{chunk-UOMW7TT6.js → chunk-I3CSD6CK.js} +1 -1
  8. package/dist/{chunk-QLXC6542.js → chunk-KIAAEHWL.js} +49 -25
  9. package/dist/{chunk-QWNXWRWO.js → chunk-LA3VTQ22.js} +25 -1
  10. package/dist/{chunk-XYD6V2LH.js → chunk-XUIWAWDO.js} +28 -33
  11. package/dist/{chunk-5CVZMHHB.js → chunk-ZTT4U4NE.js} +2 -2
  12. package/dist/{client-LWDMNKX3.js → client-KEY4PYJH.js} +1 -1
  13. package/dist/{engine-QFF2IA3G.js → engine-S3QBQ7LH.js} +2 -2
  14. package/dist/{external-edit-service-F7D3VSA2.js → external-edit-service-6FNFPOJJ.js} +3 -3
  15. package/dist/{grants-service-TBGTCO5K.js → grants-service-UT3EQ3R7.js} +2 -2
  16. package/dist/index.js +371 -90
  17. package/dist/{migrations.postgres-APCVSYUE.js → migrations.postgres-AIQ7WSU7.js} +45 -4
  18. package/dist/{review-service-A7PNLWG5.js → review-service-HHGOTO6P.js} +6 -6
  19. package/dist/{stewardship-service-32UHIRQ5.js → stewardship-service-4DHNRULG.js} +3 -3
  20. package/dist/{version-service-RJ6CWZ3O.js → version-service-KIOQME6U.js} +3 -3
  21. package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +1 -0
  22. package/dist/web3/assets/AgentDocsPage-CIiaBqMy.js +1 -0
  23. package/dist/web3/assets/CollaboratorManager-By91wrIr.js +1 -0
  24. package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +1 -0
  25. package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +36 -0
  26. package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +6 -0
  27. package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +1 -0
  28. package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +1 -0
  29. package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +643 -0
  30. package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +1 -0
  31. package/dist/web3/assets/NestView-CTZ55tXD.js +63 -0
  32. package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +1 -0
  33. package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +1 -0
  34. package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +1 -0
  35. package/dist/web3/assets/ReviewActions-OfOdqzCn.js +6 -0
  36. package/dist/web3/assets/ReviewTab-CyX8yxd3.js +1 -0
  37. package/dist/web3/assets/StewardsTab-DBEEfAQI.js +1 -0
  38. package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +1 -0
  39. package/dist/web3/assets/alert-dialog-DTozKlmV.js +7 -0
  40. package/dist/web3/assets/arrow-left-BCKt4CwQ.js +6 -0
  41. package/dist/web3/assets/backlinks-CYd4xENL.js +24 -0
  42. package/dist/web3/assets/card-XG2dP5Xf.js +1 -0
  43. package/dist/web3/assets/chevron-left-BPwUT9DS.js +6 -0
  44. package/dist/web3/assets/circle-check-DI8IBGeo.js +6 -0
  45. package/dist/web3/assets/circle-x-VKVhC88W.js +6 -0
  46. package/dist/web3/assets/code-xml-CMabYZvH.js +6 -0
  47. package/dist/web3/assets/corner-down-right-BGxwK6wH.js +6 -0
  48. package/dist/web3/assets/count-skeleton-DMbdpscX.js +1 -0
  49. package/dist/web3/assets/dates-BCxbm4_q.js +1 -0
  50. package/dist/web3/assets/earth-BEp1EKTo.js +6 -0
  51. package/dist/web3/assets/file-exclamation-point-r8qZGIjP.js +6 -0
  52. package/dist/web3/assets/folder-input-Cmtx1Jhk.js +11 -0
  53. package/dist/web3/assets/folder-target-CUSWqImF.js +1 -0
  54. package/dist/web3/assets/index-BM-h3DwI.css +1 -0
  55. package/dist/web3/assets/index-C1wTSjfP.js +29 -0
  56. package/dist/web3/assets/index-EaX2yql0.js +389 -0
  57. package/dist/web3/assets/page-B3yvQvHy.js +1 -0
  58. package/dist/web3/assets/page-BExMvttJ.js +1 -0
  59. package/dist/web3/assets/page-BUOADv2X.js +1 -0
  60. package/dist/web3/assets/page-BYUFhYWZ.js +1 -0
  61. package/dist/web3/assets/page-Bicf02Cz.js +1 -0
  62. package/dist/web3/assets/page-BsPMDm5d.js +45 -0
  63. package/dist/web3/assets/page-ByyVLDVo.js +16 -0
  64. package/dist/web3/assets/page-CC0NJi8v.js +1 -0
  65. package/dist/web3/assets/page-CHb0F5hV.js +24 -0
  66. package/dist/web3/assets/page-CPjWQNKx.js +11 -0
  67. package/dist/web3/assets/page-CQt3ZGbf.js +2 -0
  68. package/dist/web3/assets/page-Cj1NetQ-.js +1 -0
  69. package/dist/web3/assets/page-CtaW65El.js +1 -0
  70. package/dist/web3/assets/page-DGOL9l8i.js +6 -0
  71. package/dist/web3/assets/page-title-ClvvBsIo.js +1 -0
  72. package/dist/web3/assets/play-C5YNKpAH.js +6 -0
  73. package/dist/web3/assets/refresh-cw-BhHzSH9m.js +6 -0
  74. package/dist/web3/assets/scroll-area-dRWncRqa.css +1 -0
  75. package/dist/web3/assets/scroll-area-hz-xtayP.js +1 -0
  76. package/dist/web3/assets/select-DBFTrUmI.js +6 -0
  77. package/dist/web3/assets/send-0CHvmluT.js +6 -0
  78. package/dist/web3/assets/settings-CeeF4LEb.js +6 -0
  79. package/dist/web3/assets/share-2-CMnOlOt-.js +6 -0
  80. package/dist/web3/assets/tag-DQ_6J5Gv.js +11 -0
  81. package/dist/web3/assets/trash-2-HBi-Pslz.js +6 -0
  82. package/dist/web3/assets/triangle-alert-CDy8-7sv.js +6 -0
  83. package/dist/web3/assets/user-plus-CvBNcpn4.js +6 -0
  84. package/dist/web3/assets/x-By7piikG.js +6 -0
  85. package/dist/web3/assets/zap-C2T8riBp.js +11 -0
  86. package/dist/web3/index.html +2 -2
  87. package/package.json +4 -2
  88. package/dist/web3/assets/index-BJ-LRNis.js +0 -1382
  89. package/dist/web3/assets/index-DPMEt-A_.css +0 -1
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  submitForReview,
39
39
  withNodeWriteLock,
40
40
  withdrawDeletionRequest
41
- } from "./chunk-MLQU4I5J.js";
41
+ } from "./chunk-5EZOPA47.js";
42
42
  import {
43
43
  canCreateInNest,
44
44
  canManageStewards,
@@ -58,7 +58,7 @@ import {
58
58
  resolveUserRoles,
59
59
  syncFromConfig,
60
60
  updateSteward
61
- } from "./chunk-2YPY2HGZ.js";
61
+ } from "./chunk-2TPQTN4Y.js";
62
62
  import {
63
63
  createGrant,
64
64
  deleteGrant,
@@ -67,7 +67,7 @@ import {
67
67
  listGrants,
68
68
  listUserGrants,
69
69
  resolveNodeGrant
70
- } from "./chunk-UOMW7TT6.js";
70
+ } from "./chunk-I3CSD6CK.js";
71
71
  import {
72
72
  generateApiKey,
73
73
  getKeyPrefix,
@@ -93,7 +93,7 @@ import {
93
93
  listMyDrafts,
94
94
  setApprovedVersion,
95
95
  upsertVersion
96
- } from "./chunk-5CVZMHHB.js";
96
+ } from "./chunk-ZTT4U4NE.js";
97
97
  import {
98
98
  approveExternalEdit,
99
99
  getExternalEditDetail,
@@ -105,7 +105,7 @@ import {
105
105
  scanDocumentForDrift,
106
106
  scanNestForDrift,
107
107
  startDriftScanner
108
- } from "./chunk-XYD6V2LH.js";
108
+ } from "./chunk-XUIWAWDO.js";
109
109
  import {
110
110
  addMember,
111
111
  addWatcher,
@@ -211,7 +211,7 @@ import {
211
211
  updateMemberRole,
212
212
  validateLicense,
213
213
  verifySmtp
214
- } from "./chunk-QLXC6542.js";
214
+ } from "./chunk-KIAAEHWL.js";
215
215
  import {
216
216
  AppError,
217
217
  ConflictError,
@@ -229,7 +229,7 @@ import {
229
229
  initDb,
230
230
  isEmailListish,
231
231
  isEmailish
232
- } from "./chunk-QWNXWRWO.js";
232
+ } from "./chunk-LA3VTQ22.js";
233
233
  import {
234
234
  ANON_EMAIL,
235
235
  ANON_USER_ID,
@@ -1696,6 +1696,15 @@ authRoutes.get("/admin-status", async (c) => {
1696
1696
  me
1697
1697
  });
1698
1698
  });
1699
+ authRoutes.patch("/profile", authMiddleware, async (c) => {
1700
+ const userId = c.get("userId");
1701
+ const body = await c.req.json();
1702
+ if (typeof body.name !== "string") throw new ValidationError("name is required");
1703
+ const name = body.name.trim();
1704
+ if (name.length > 80) throw new ValidationError("name must be 80 characters or fewer");
1705
+ await getDb().run("UPDATE users SET name = ? WHERE id = ?", [name || null, userId]);
1706
+ return c.json({ ok: true, name: name || null });
1707
+ });
1699
1708
  authRoutes.post("/password", authMiddleware, async (c) => {
1700
1709
  const body = await c.req.json();
1701
1710
  if (!body.current || !body.next) {
@@ -2559,6 +2568,19 @@ var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
2559
2568
  "artifact",
2560
2569
  "table"
2561
2570
  ]);
2571
+ function assertAcceptedType(type) {
2572
+ if (type === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
2573
+ throw new ValidationError("Artifact nodes are disabled on this server");
2574
+ }
2575
+ if (type === "table" && !config.TYPE_TABLE_ENABLED) {
2576
+ throw new ValidationError("Table nodes are disabled on this server");
2577
+ }
2578
+ if (!ACCEPTED_NODE_TYPES.has(type)) {
2579
+ throw new ValidationError(
2580
+ `unknown node type "${type}"; allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")}`
2581
+ );
2582
+ }
2583
+ }
2562
2584
  function toNodeResponse(node) {
2563
2585
  const fm = node.frontmatter;
2564
2586
  const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
@@ -3009,17 +3031,7 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
3009
3031
  }
3010
3032
  const tags = requireTags(input.tags);
3011
3033
  const requestedType = input.type || "document";
3012
- if (requestedType === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
3013
- throw new ValidationError("Artifact nodes are disabled on this server");
3014
- }
3015
- if (requestedType === "table" && !config.TYPE_TABLE_ENABLED) {
3016
- throw new ValidationError("Table nodes are disabled on this server");
3017
- }
3018
- if (!ACCEPTED_NODE_TYPES.has(requestedType)) {
3019
- throw new ValidationError(
3020
- `unknown node type "${requestedType}"; allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")}`
3021
- );
3022
- }
3034
+ assertAcceptedType(requestedType);
3023
3035
  if (input.schedule !== void 0 && !isRunnableType(input.type)) {
3024
3036
  throw new ValidationError(
3025
3037
  `schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
@@ -3282,6 +3294,20 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
3282
3294
  };
3283
3295
  opInput.title = patch.title;
3284
3296
  }
3297
+ if (patch.type !== void 0 && patch.type !== node.frontmatter.type) {
3298
+ assertAcceptedType(patch.type);
3299
+ if (patch.type === "skill") {
3300
+ opInput.trigger = `when asked to ${String(node.frontmatter.title || nodeId).toLowerCase()}`;
3301
+ }
3302
+ if (isRunnableType(node.frontmatter.type) && !isRunnableType(patch.type)) {
3303
+ const metadata = { ...node.frontmatter.metadata };
3304
+ delete metadata.schedule;
3305
+ node = { ...node, frontmatter: { ...node.frontmatter, metadata } };
3306
+ opInput.metadata = { ...opInput.metadata, schedule: null };
3307
+ }
3308
+ node = { ...node, frontmatter: { ...node.frontmatter, type: patch.type } };
3309
+ opInput.type = patch.type;
3310
+ }
3285
3311
  if (patch.schedule !== void 0) {
3286
3312
  if (!isRunnableType(node.frontmatter.type)) {
3287
3313
  throw new ValidationError(
@@ -4590,12 +4616,19 @@ sharingRoutes.patch("/reader-mode", async (c) => {
4590
4616
  import { Hono as Hono5 } from "hono";
4591
4617
  import { v4 as uuid7 } from "uuid";
4592
4618
  var grantRoutes = new Hono5();
4593
- async function requireManage(c, nestId) {
4594
- if (!await canManageStewards(nestId, c.get("userId"))) {
4619
+ async function requireManage(c, nestId, opts = {}) {
4620
+ if (config.AUTH_MODE === "open") return;
4621
+ const access = await resolveNestAccess(nestId, c.get("userId"));
4622
+ if (access.rawPermission !== "owner" && access.rawPermission !== "admin") {
4595
4623
  throw new ForbiddenError(
4596
4624
  "Only a nest admin, the nest owner, or the server admin can manage sharing."
4597
4625
  );
4598
4626
  }
4627
+ if (access.archived && !opts.readOnly) {
4628
+ throw new ForbiddenError(
4629
+ "This nest is archived, so it is read-only. Restore it to change sharing."
4630
+ );
4631
+ }
4599
4632
  }
4600
4633
  async function resolveGrantee(email) {
4601
4634
  const db = getDb();
@@ -4685,7 +4718,7 @@ grantRoutes.post("/", async (c) => {
4685
4718
  });
4686
4719
  grantRoutes.get("/", async (c) => {
4687
4720
  const nestId = c.req.param("nestId");
4688
- await requireManage(c, nestId);
4721
+ await requireManage(c, nestId, { readOnly: true });
4689
4722
  const grants = await listGrants(nestId);
4690
4723
  const db = getDb();
4691
4724
  const withEmail = await Promise.all(
@@ -6208,7 +6241,7 @@ nodeRoutes.post("/", async (c) => {
6208
6241
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
6209
6242
  const nestId = c.req.param("nestId");
6210
6243
  const nodeId = c.req.param("nodeId");
6211
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-32UHIRQ5.js");
6244
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-4DHNRULG.js");
6212
6245
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
6213
6246
  nestId,
6214
6247
  nodeId
@@ -6229,7 +6262,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
6229
6262
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
6230
6263
  const nestId = c.req.param("nestId");
6231
6264
  const nodeId = c.req.param("nodeId");
6232
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-A7PNLWG5.js");
6265
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-HHGOTO6P.js");
6233
6266
  const history = await getReviewHistory2(nestId, nodeId);
6234
6267
  return c.json({ reviews: history });
6235
6268
  });
@@ -6368,7 +6401,8 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
6368
6401
  }
6369
6402
  }
6370
6403
  response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
6371
- if (publicReader) {
6404
+ const approvedOnly = c.req.query("approved_only") === "1" || c.req.query("approved_only") === "true";
6405
+ if (publicReader || approvedOnly) {
6372
6406
  const approved = await getApprovedVersion(nestId, nodeId);
6373
6407
  if (approved != null) {
6374
6408
  let approvedRaw = "";
@@ -6399,6 +6433,9 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
6399
6433
  }
6400
6434
  return c.json({ node: response });
6401
6435
  }
6436
+ if (approvedOnly) {
6437
+ throw new NotFoundError("This document has no approved version yet.");
6438
+ }
6402
6439
  }
6403
6440
  response.status = node.pendingChange ? "external_edit_pending" : await getDisplayStatus(nestId, nodeId);
6404
6441
  if (response.status === "pending_review") {
@@ -6423,6 +6460,9 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
6423
6460
  const nestId = c.req.param("nestId");
6424
6461
  const nodeId = c.req.param("nodeId");
6425
6462
  const body = await c.req.json();
6463
+ if (body.type !== void 0 && typeof body.type !== "string") {
6464
+ throw new ValidationError("type must be a string");
6465
+ }
6426
6466
  const baseVersionHeader = c.req.header("X-Base-Version");
6427
6467
  if (baseVersionHeader) {
6428
6468
  const baseVersion = parseInt(baseVersionHeader, 10);
@@ -6453,7 +6493,8 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
6453
6493
  status: body.status,
6454
6494
  changeNote: body.changeNote,
6455
6495
  schedule: body.schedule,
6456
- prime: body.prime
6496
+ prime: body.prime,
6497
+ type: body.type
6457
6498
  },
6458
6499
  authorEmail
6459
6500
  );
@@ -6957,7 +6998,18 @@ var STOPWORDS = /* @__PURE__ */ new Set([
6957
6998
  "ok",
6958
6999
  "please",
6959
7000
  "thanks",
6960
- "thank"
7001
+ "thank",
7002
+ // Greetings and fillers a chat turn opens with — never a topic.
7003
+ "hello",
7004
+ "hi",
7005
+ "hey",
7006
+ "there",
7007
+ "good",
7008
+ "morning",
7009
+ "afternoon",
7010
+ "evening",
7011
+ "bye",
7012
+ "cheers"
6961
7013
  ]);
6962
7014
  function tokenizePrompt(prompt) {
6963
7015
  const words = prompt.toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean).filter((w) => w.length >= 3 && !STOPWORDS.has(w));
@@ -6999,6 +7051,34 @@ async function compilePrompt(prompt, nestId, titles) {
6999
7051
  unmatched
7000
7052
  };
7001
7053
  }
7054
+ function scorePrompt(doc, tokens) {
7055
+ if (tokens.length === 0) return { score: 0, via: null };
7056
+ const title = doc.frontmatter.title || "";
7057
+ const tags = (doc.frontmatter.tags || []).map(normalizeTag);
7058
+ const body = (doc.body || "").toLowerCase();
7059
+ let sum = 0;
7060
+ let via = null;
7061
+ for (const t of tokens) {
7062
+ const inTitle = titleScore(title, t);
7063
+ if (inTitle > 0) {
7064
+ sum += inTitle;
7065
+ via = "title";
7066
+ } else if (tags.some((tag) => tag.startsWith(t))) {
7067
+ sum += 2;
7068
+ if (via !== "title") via = "tag";
7069
+ } else if (startsWord(body, t)) {
7070
+ sum += 1;
7071
+ via ??= "fulltext";
7072
+ }
7073
+ }
7074
+ return { score: sum / (4 * tokens.length), via };
7075
+ }
7076
+ function startsWord(text, term) {
7077
+ for (let i = text.indexOf(term); i !== -1; i = text.indexOf(term, i + 1)) {
7078
+ if (i === 0 || !/[a-z0-9]/.test(text[i - 1])) return true;
7079
+ }
7080
+ return false;
7081
+ }
7002
7082
 
7003
7083
  // src/nodes/readable-body.ts
7004
7084
  async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
@@ -7393,7 +7473,8 @@ queryRoutes.post("/context", async (c) => {
7393
7473
  const baseUrl = requestBaseUrl(c.req.url);
7394
7474
  const { query: queryEngine, storage } = await engineCache.get(nestId);
7395
7475
  const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
7396
- const hops = body.hops ?? 2;
7476
+ const promptPath = !body.selector?.trim() && !!body.prompt;
7477
+ const hops = body.hops ?? (promptPath ? 0 : 2);
7397
7478
  const includeDrafts = body.include_drafts === true;
7398
7479
  const isVisible = (d) => includeDrafts || d.frontmatter.status === "published";
7399
7480
  let selector = body.selector?.trim() || null;
@@ -7427,7 +7508,7 @@ queryRoutes.post("/context", async (c) => {
7427
7508
  compileDetail.matchedTitles.map((t) => t.toLowerCase())
7428
7509
  );
7429
7510
  titleMatches = allDocs.filter(
7430
- (d) => matchedLower.has(d.frontmatter.title.toLowerCase())
7511
+ (d) => matchedLower.has(d.frontmatter.title.toLowerCase()) && isVisible(d)
7431
7512
  );
7432
7513
  }
7433
7514
  }
@@ -7491,6 +7572,27 @@ queryRoutes.post("/context", async (c) => {
7491
7572
  }
7492
7573
  }
7493
7574
  }
7575
+ const relevance = /* @__PURE__ */ new Map();
7576
+ let fallback = null;
7577
+ if (compileDetail) {
7578
+ const tokens = tokenizePrompt(body.prompt);
7579
+ const compiled = new Set(documents.map((d) => d.id));
7580
+ for (const d of allDocs) {
7581
+ const { score, via } = scorePrompt(d, tokens);
7582
+ if (compiled.has(d.id)) relevance.set(d.id, { score, via: via ?? "hop" });
7583
+ else if (score > 0 && isVisible(d)) {
7584
+ relevance.set(d.id, { score, via });
7585
+ documents.push(d);
7586
+ }
7587
+ }
7588
+ if (compiled.size === 0 && documents.length > 0) fallback = "fulltext";
7589
+ const top = Math.max(0, ...documents.map((d) => relevance.get(d.id).score));
7590
+ documents = documents.filter((d) => {
7591
+ const r = relevance.get(d.id);
7592
+ return compiled.has(d.id) || r.via === "hop" || r.score >= top / 2;
7593
+ });
7594
+ documents.sort((a, b) => relevance.get(b.id).score - relevance.get(a.id).score);
7595
+ }
7494
7596
  const userId = c.get("userId");
7495
7597
  const userEmail = await resolveCallerEmail(userId);
7496
7598
  const beforePermission = documents.length;
@@ -7508,10 +7610,14 @@ queryRoutes.post("/context", async (c) => {
7508
7610
  for (const doc of readable) {
7509
7611
  const block = formatContextBlock(doc, doc.url);
7510
7612
  const blockTokens = approxTokens(block);
7511
- if (tokenCount + blockTokens > maxTokens && included.length > 0) break;
7613
+ if (tokenCount + blockTokens > maxTokens) continue;
7512
7614
  included.push(doc);
7513
7615
  tokenCount += blockTokens;
7514
7616
  }
7617
+ if (included.length === 0 && readable.length > 0) {
7618
+ included.push(readable[0]);
7619
+ tokenCount = approxTokens(formatContextBlock(readable[0], readable[0].url));
7620
+ }
7515
7621
  const truncatedByBudget = accessible.length - included.length;
7516
7622
  const contextMarkdown = included.map((n) => formatContextBlock(n, n.url)).join("\n\n---\n\n");
7517
7623
  trackEvent("context.resolve", {
@@ -7528,7 +7634,8 @@ queryRoutes.post("/context", async (c) => {
7528
7634
  title: n.frontmatter.title,
7529
7635
  url: n.url,
7530
7636
  tags: n.frontmatter.tags || [],
7531
- type: n.frontmatter.type || "document"
7637
+ type: n.frontmatter.type || "document",
7638
+ ...relevance.get(n.id)
7532
7639
  })),
7533
7640
  trace: {
7534
7641
  selector_used: selector,
@@ -7536,7 +7643,8 @@ queryRoutes.post("/context", async (c) => {
7536
7643
  compiled: compileDetail ? {
7537
7644
  matched_tags: compileDetail.matchedTags,
7538
7645
  matched_titles: compileDetail.matchedTitles,
7539
- unmatched_tokens: compileDetail.unmatched
7646
+ unmatched_tokens: compileDetail.unmatched,
7647
+ fallback
7540
7648
  } : null,
7541
7649
  total_matched: documents.length,
7542
7650
  permission_filtered: permissionFiltered,
@@ -7721,11 +7829,11 @@ queryRoutes.get("/export", async (c) => {
7721
7829
  let tokens = 0;
7722
7830
  for (const f of fields) {
7723
7831
  const t = approxTokens(nodeToMarkdown(f));
7724
- if (tokens + t > maxTokens && kept.length > 0) break;
7832
+ if (tokens + t > maxTokens) continue;
7725
7833
  kept.push(f);
7726
7834
  tokens += t;
7727
7835
  }
7728
- included = kept;
7836
+ included = kept.length > 0 ? kept : fields.slice(0, 1);
7729
7837
  }
7730
7838
  trackEvent("nest.export", {
7731
7839
  nestId,
@@ -10256,21 +10364,20 @@ ${list}${more}
10256
10364
  Submit one with \`context_submit_review\`.`;
10257
10365
  }
10258
10366
  case "context_submit_review": {
10259
- const docs = await storage.discoverDocuments();
10260
- const node = docs.find(
10261
- (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
10262
- );
10263
- if (!node) return toolError(`Node not found: ${args.title}`, "DOCUMENT_NOT_FOUND");
10367
+ const resolved = await resolveTargetNode(storage, args);
10368
+ if (typeof resolved === "string") return toolError(resolved, "DOCUMENT_NOT_FOUND");
10369
+ const node = resolved;
10370
+ const nodeLabel = args.title || args.id;
10264
10371
  const submitCheck = await canUserEdit(ctx.nestId, node.id, userEmail);
10265
10372
  if (!submitCheck.allowed) {
10266
10373
  return toolError(
10267
- `You don't have permission to submit "${args.title}" for review: ${submitCheck.reason}`,
10374
+ `You don't have permission to submit "${nodeLabel}" for review: ${submitCheck.reason}`,
10268
10375
  "UNAUTHORIZED_ACTION"
10269
10376
  );
10270
10377
  }
10271
10378
  const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
10272
10379
  if (currentVersion === 0)
10273
- return toolError(`No versions found for "${args.title}"`, "VERSION_NOT_FOUND");
10380
+ return toolError(`No versions found for "${nodeLabel}"`, "VERSION_NOT_FOUND");
10274
10381
  try {
10275
10382
  const request = await submitForReview({
10276
10383
  nestId: ctx.nestId,
@@ -10281,16 +10388,16 @@ Submit one with \`context_submit_review\`.`;
10281
10388
  priority: args.priority,
10282
10389
  baseUrl: ctx.baseUrl
10283
10390
  });
10284
- const resolved = await resolveStewardsForNode(
10391
+ const resolved2 = await resolveStewardsForNode(
10285
10392
  ctx.nestId,
10286
10393
  node.id
10287
10394
  );
10288
- const stewardList = resolved.length > 0 ? `
10395
+ const stewardList = resolved2.length > 0 ? `
10289
10396
 
10290
10397
  **Stewards who can review:**
10291
- ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "";
10398
+ ${resolved2.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "";
10292
10399
  return {
10293
- text: `Submitted "${args.title}" v${currentVersion} for review (${request.priority} priority).${stewardList}`,
10400
+ text: `Submitted "${nodeLabel}" v${currentVersion} for review (${request.priority} priority).${stewardList}`,
10294
10401
  // No catalog shape to conform to — `context_submit_review` is a
10295
10402
  // governance-namespace operation and that namespace is declared but
10296
10403
  // unimplemented in the engine (`api/index.ts`, NAMESPACES). So this
@@ -10311,7 +10418,7 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
10311
10418
  requested_by: request.requestedBy,
10312
10419
  requested_at: request.requestedAt
10313
10420
  },
10314
- reviewers: resolved.map((r) => r.steward.userEmail)
10421
+ reviewers: resolved2.map((r) => r.steward.userEmail)
10315
10422
  }
10316
10423
  };
10317
10424
  } catch (err) {
@@ -10456,8 +10563,8 @@ ${list}`,
10456
10563
  if (!target) return "target is required (a node id or folder prefix).";
10457
10564
  if (!["read", "write"].includes(role)) return "role must be read or write.";
10458
10565
  try {
10459
- const { createGrant: createGrant2 } = await import("./grants-service-TBGTCO5K.js");
10460
- const db = (await import("./client-LWDMNKX3.js")).getDb();
10566
+ const { createGrant: createGrant2 } = await import("./grants-service-UT3EQ3R7.js");
10567
+ const db = (await import("./client-KEY4PYJH.js")).getDb();
10461
10568
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
10462
10569
  const e = normalizeEmail2(String(args.email || ""));
10463
10570
  if (!e) return "email is required.";
@@ -11747,7 +11854,20 @@ import { Hono as Hono23 } from "hono";
11747
11854
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
11748
11855
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
11749
11856
  import { z as z2 } from "zod";
11750
- var SERVER_WRITE_TOOLS = /* @__PURE__ */ new Set(["nest_create", "context_create"]);
11857
+ var SERVER_WRITE_TOOLS = /* @__PURE__ */ new Set([
11858
+ "nest_create",
11859
+ "context_create",
11860
+ "context_update",
11861
+ "context_delete",
11862
+ "context_move",
11863
+ "context_submit_review"
11864
+ ]);
11865
+ var FANOUT_READ_TOOLS = /* @__PURE__ */ new Set([
11866
+ "context_search",
11867
+ "context_list",
11868
+ "context_query",
11869
+ "context_overview"
11870
+ ]);
11751
11871
  async function getUserEmail3(userId) {
11752
11872
  const db = getDb();
11753
11873
  const user = await db.get("SELECT email FROM users WHERE id = ?", [
@@ -11847,77 +11967,83 @@ var NEST_ARG = {
11847
11967
  description: "Target nest \u2014 id, or exact name/slug (case-insensitive)"
11848
11968
  }
11849
11969
  };
11970
+ var NEST_ARG_OPTIONAL = {
11971
+ nest: {
11972
+ type: "string",
11973
+ description: "Target nest \u2014 id or name. Omit to search/list across ALL accessible nests."
11974
+ }
11975
+ };
11850
11976
  var SERVER_TOOL_DEFINITIONS = [
11851
11977
  {
11852
11978
  name: "nest_index",
11853
- description: "Grounded index of every nest these credentials can read: name, id, permission, document count. Call this FIRST, then target a nest with the other tools.",
11979
+ description: "Grounded index of every nest these credentials can read: name, id, permission, document count. Call this FIRST.",
11854
11980
  inputSchema: { type: "object", properties: {} }
11855
11981
  },
11982
+ // ─── Fan-out reads (nest optional) ─────────────────────────────────────
11856
11983
  {
11857
11984
  name: "context_query",
11858
- description: "Run a structured selector query against one nest. Selector grammar: #tag, type:X, [[Title]], scope:X, combined with +AND, |OR, -NOT.",
11985
+ description: "Run a structured selector query. Omit `nest` to query ALL accessible nests. Selector: #tag, type:X, [[Title]], scope:X, +AND, |OR, -NOT.",
11859
11986
  inputSchema: {
11860
11987
  type: "object",
11861
11988
  properties: {
11862
- ...NEST_ARG,
11989
+ ...NEST_ARG_OPTIONAL,
11863
11990
  query: { type: "string", description: "Selector query" },
11864
11991
  hops: {
11865
11992
  type: "number",
11866
11993
  description: "Graph traversal depth from the matches (default: 2)"
11867
11994
  }
11868
11995
  },
11869
- required: ["nest", "query"]
11996
+ required: ["query"]
11870
11997
  }
11871
11998
  },
11872
11999
  {
11873
12000
  name: "context_search",
11874
- description: "Full-text keyword search across one nest \u2014 the same index-backed match as REST GET /search. Returns `count` (everything found) and the first `limit` hits.",
12001
+ description: "Full-text keyword search. Omit `nest` to search ALL accessible nests. Returns count and first `limit` hits.",
11875
12002
  inputSchema: {
11876
12003
  type: "object",
11877
12004
  properties: {
11878
- ...NEST_ARG,
12005
+ ...NEST_ARG_OPTIONAL,
11879
12006
  query: { type: "string", description: "Search terms" },
11880
12007
  limit: {
11881
12008
  type: "number",
11882
12009
  description: "Max hits to return (default 20, max 200)"
11883
12010
  }
11884
12011
  },
11885
- required: ["nest", "query"]
11886
- }
11887
- },
11888
- {
11889
- name: "context_get",
11890
- description: "Get the FULL content of a node in a nest by title or id.",
11891
- inputSchema: {
11892
- type: "object",
11893
- properties: {
11894
- ...NEST_ARG,
11895
- title: { type: "string", description: "Title of the node" },
11896
- id: { type: "string", description: "ID of the node" }
11897
- },
11898
- required: ["nest"]
12012
+ required: ["query"]
11899
12013
  }
11900
12014
  },
11901
12015
  {
11902
12016
  name: "context_list",
11903
- description: "Browse one nest's contents with optional type, tag, or limit filters.",
12017
+ description: "Browse contents with optional type, tag, or limit filters. Omit `nest` to list ALL accessible nests.",
11904
12018
  inputSchema: {
11905
12019
  type: "object",
11906
12020
  properties: {
11907
- ...NEST_ARG,
12021
+ ...NEST_ARG_OPTIONAL,
11908
12022
  type: { type: "string", description: "Filter by node type" },
11909
12023
  tag: { type: "string", description: "Filter by tag" },
11910
12024
  limit: { type: "number", description: "Max nodes to return" }
11911
- },
11912
- required: ["nest"]
12025
+ }
11913
12026
  }
11914
12027
  },
11915
12028
  {
11916
12029
  name: "context_overview",
11917
- description: "Map of one nest: node count, types, tags, title+snippet per node.",
12030
+ description: "Map of nests: node count, types, tags, title+snippet per node. Omit `nest` for ALL accessible nests.",
11918
12031
  inputSchema: {
11919
12032
  type: "object",
11920
- properties: { ...NEST_ARG },
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
+ },
11921
12047
  required: ["nest"]
11922
12048
  }
11923
12049
  },
@@ -11969,9 +12095,10 @@ var SERVER_TOOL_DEFINITIONS = [
11969
12095
  required: ["name"]
11970
12096
  }
11971
12097
  },
12098
+ // ─── Writes (nest required) ────────────────────────────────────────────
11972
12099
  {
11973
12100
  name: "context_create",
11974
- description: "Create a new knowledge node (document) in a nest. Needs write permission on that nest. A `folder` path creates the folder implicitly \u2014 folders exist through the nodes placed in them.",
12101
+ description: "Create a new document in a nest. Needs write permission. A `folder` path creates the folder implicitly.",
11975
12102
  inputSchema: {
11976
12103
  type: "object",
11977
12104
  properties: {
@@ -11988,6 +12115,66 @@ var SERVER_TOOL_DEFINITIONS = [
11988
12115
  },
11989
12116
  required: ["nest", "title", "content"]
11990
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
+ }
11991
12178
  }
11992
12179
  ];
11993
12180
  async function handleServerToolCall(toolName, args, userId, userEmail, nestScope, baseUrl) {
@@ -12021,8 +12208,49 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
12021
12208
  throw err;
12022
12209
  }
12023
12210
  }
12024
- const inaccessible = `Nest not found or not accessible: "${args.nest}". Use nest_index to see what these credentials can read.`;
12025
- const nest = await resolveNestRef(String(args.nest ?? ""), userId);
12211
+ const nestArg = args.nest;
12212
+ if (!nestArg && FANOUT_READ_TOOLS.has(toolName)) {
12213
+ const nests = await buildNestIndex(userId, nestScope);
12214
+ if (!nests.length) return "No nests accessible with these credentials.";
12215
+ 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
+ const results = await Promise.allSettled(
12220
+ nonEmpty.map(async (n) => {
12221
+ const engine2 = await engineCache.get(n.id);
12222
+ const result = await handleToolCall(toolName, args, {
12223
+ storage: engine2.storage,
12224
+ queryEngine: engine2.query,
12225
+ versionManager: engine2.versions,
12226
+ nestId: n.id,
12227
+ userId,
12228
+ userEmail,
12229
+ baseUrl
12230
+ });
12231
+ return { nest: n, result };
12232
+ })
12233
+ );
12234
+ const parts = [];
12235
+ for (const r of results) {
12236
+ if (r.status === "fulfilled") {
12237
+ const { nest: n, result } = r.value;
12238
+ const text = typeof result === "string" ? result : result.text;
12239
+ parts.push(`## ${n.name} (${n.id})
12240
+
12241
+ ${text}`);
12242
+ }
12243
+ }
12244
+ if (!parts.length) {
12245
+ return "No results found across any accessible nests.";
12246
+ }
12247
+ return parts.join("\n\n---\n\n");
12248
+ }
12249
+ const inaccessible = `Nest not found or not accessible: "${nestArg}". Use nest_index to see what these credentials can read.`;
12250
+ if (!nestArg) {
12251
+ return "This tool requires a `nest` argument. Use nest_index to see available nests.";
12252
+ }
12253
+ const nest = await resolveNestRef(String(nestArg), userId);
12026
12254
  if (!nest) return inaccessible;
12027
12255
  if (nestScope && nest.id !== nestScope) return inaccessible;
12028
12256
  if (!await canRead(nest.id, userId)) return inaccessible;
@@ -12164,26 +12392,47 @@ Other read endpoints (all under \`${B}/nests/<nest-id>\`):
12164
12392
  - \`GET /nodes/<node-id>?format=raw\` \u2014 direct consumption path: the node BODY as plain text (no JSON envelope; add \`&frontmatter=1\` for the stored YAML block)
12165
12393
  - \`GET /export?format=markdown\` \u2014 whole nest as one markdown file (optional \`selector\`, \`max_tokens\`)
12166
12394
  - \`GET /graph\` \u2014 ontology graph (nodes, links, tags, stewards)
12395
+ - \`GET /folders\` \u2014 folder tree; \`GET /nodes?folder=<path>\` \u2014 one folder level
12396
+ - \`POST /table-query\` \u2014 exact rows from a table node: \`{ "table": "[[Title]]", "query": "where <col> <op> <value> [select cols] [limit N]" }\`
12397
+ - \`GET /nodes/<node-id>/versions\` \u2014 history (who, when, approval state); \`/versions/<n>\` \u2014 one version
12398
+ - \`GET /nodes/<node-id>/comments\` \u2014 review threads (anchored quotes, replies, open/resolved)
12167
12399
 
12168
12400
  Write endpoints (need write permission):
12169
12401
  - \`POST /nodes\` \u2014 \`{ "title", "content", "tags"?, "type"?, "folder"? }\`
12170
12402
  - \`PATCH /nodes/<node-id>\` \u2014 \`{ "content"? , "append"?, "tags"?, "title"? }\`
12171
- - \`DELETE /nodes/<node-id>\`
12172
- - Governance: \`POST /nodes/<id>/submit-review\`, \`/approve\`, \`/reject\`
12403
+ - \`DELETE /nodes/<node-id>\`; \`POST /nodes/<id>/request-deletion\` when you lack delete rights
12404
+ - \`POST /nodes/<id>/move\` \u2014 \`{ "folder" }\`; history and comments follow
12405
+ - \`POST /nodes/<id>/revert\` \u2014 \`{ "targetVersion" }\` restores a past version as a new one
12406
+ - \`POST /nodes/<id>/comments\` \u2014 leave or reply to a review comment
12407
+ - Governance: \`POST /nodes/<id>/submit-review\`, \`/approve\`, \`/reject\`, \`/cancel-review\`
12408
+ - \`GET /review-queue\` \u2014 what is waiting on a steward in this nest
12409
+
12410
+ ## Your inbox (server-level)
12411
+
12412
+ - \`GET ${B}/me/work\` \u2014 reviews waiting on you and handoffs, across every nest
12413
+ - \`GET ${B}/me/drafts\` \u2014 your documents never submitted for review
12414
+ - \`GET ${B}/me/notifications\` \u2014 watcher and review notifications
12173
12415
 
12174
12416
  ## MCP (Model Context Protocol)
12175
12417
 
12176
12418
  Streamable HTTP, stateless. Two endpoints:
12177
12419
 
12178
- - \`${B}/mcp\` \u2014 server-level, one connection for everything readable.
12179
- Tools: \`nest_index\` (call first), then \`context_query\`, \`context_search\`,
12180
- \`context_get\`, \`context_list\`, \`context_overview\`, \`context_resolve\` \u2014
12181
- each takes a \`nest\` argument (id or exact name) plus the same
12182
- selector/hops/max_tokens parameters as the REST API. Two bootstrap writes:
12183
- \`nest_create\` (new nest owned by the caller) and \`context_create\`
12184
- (new document in a writable nest; a \`folder\` path creates the folder).
12185
- - \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, full toolset including writes and
12186
- governance (create/update, submit/approve/reject, stewards, sharing).
12420
+ - \`${B}/mcp\` \u2014 server-level, one connection for everything.
12421
+ Tools: \`nest_index\` (call first), then:
12422
+ - Fan-out reads (omit \`nest\` to search/list ALL nests): \`context_search\`,
12423
+ \`context_list\`, \`context_query\`, \`context_overview\`
12424
+ - Targeted reads (\`nest\` required): \`context_get\`, \`context_resolve\`, \`context_comments\`
12425
+ - Writes (\`nest\` required): \`nest_create\`, \`context_create\`, \`context_update\`,
12426
+ \`context_delete\`, \`context_move\`, \`context_submit_review\`
12427
+ - \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, the full toolset. Same names as the
12428
+ server-level tools without the \`nest\` argument, plus:
12429
+ - read: \`context_init\` (operating instructions \u2014 call first), \`context_export\`, \`context_table_query\`, \`context_versions\`
12430
+ - write: \`context_update\`, \`context_delete\`, \`context_move\`, \`context_request_deletion\`
12431
+ - review: \`context_submit_review\`, \`context_approve\`, \`context_reject\`, \`context_review_queue\`, \`context_my_drafts\`
12432
+ - comments: \`context_comment\`, \`context_resolve_comment\`
12433
+ - people: \`context_stewards\`, \`context_assign_steward\`, \`context_share_nest\`, \`context_grant\`
12434
+ - admin: \`context_deletion_queue\`, \`context_resolve_deletion\`, \`context_unsynced_list\`, \`context_sync_folder\`
12435
+ - \`workflow_*\` (runs, edges, edge types) when the nest's workflow plane is on
12187
12436
  Sharing a slice is one call: \`context_grant\` with
12188
12437
  \`{ "email", "target_type": "document" | "folder", "target", "role": "read" | "write" }\`
12189
12438
  \u2014 \`target\` is a node id (\`nodes/win-loss-log\`) or a folder prefix
@@ -13022,6 +13271,7 @@ governanceRoutes.get("/external-edits", async (c) => {
13022
13271
  await scanNestForDrift(nestId, "user:refresh");
13023
13272
  }
13024
13273
  const entries = await listNestExternalEdits(nestId);
13274
+ if (c.req.query("count") === "1") return c.json({ total: entries.length });
13025
13275
  return c.json({ entries, total: entries.length });
13026
13276
  });
13027
13277
  governanceRoutes.post("/external-edits/scan", async (c) => {
@@ -13488,6 +13738,21 @@ async function ensureAnonymousUser() {
13488
13738
  return ANON_USER_ID;
13489
13739
  }
13490
13740
 
13741
+ // src/shared/posthog-key.ts
13742
+ var POSTHOG_KEY_MIN_TOKEN = 20;
13743
+ function posthogKeyError(value) {
13744
+ const v = value.trim();
13745
+ if (!v) return null;
13746
+ if (!v.startsWith("phc_"))
13747
+ return "PostHog project key must start with phc_ \u2014 copy the project API key from PostHog settings.";
13748
+ const token = v.slice(4);
13749
+ if (!/^[A-Za-z0-9]*$/.test(token))
13750
+ return "PostHog project key may only contain letters and digits after phc_ \u2014 check for spaces or punctuation.";
13751
+ if (token.length < POSTHOG_KEY_MIN_TOKEN)
13752
+ return `PostHog project key looks too short \u2014 a real key is about 47 characters long (phc_ plus ${POSTHOG_KEY_MIN_TOKEN}+ letters and digits). Copy the whole key from PostHog settings.`;
13753
+ return null;
13754
+ }
13755
+
13491
13756
  // src/app.ts
13492
13757
  import { serveStatic } from "@hono/node-server/serve-static";
13493
13758
  import { fileURLToPath } from "url";
@@ -13901,7 +14166,9 @@ function createApp() {
13901
14166
  }
13902
14167
  if ("posthog_key" in body) {
13903
14168
  const v = String(body.posthog_key ?? "").trim();
13904
- pending.push({ name: "POSTHOG_KEY", value: v || null });
14169
+ const keyError = posthogKeyError(v);
14170
+ if (keyError) addError("posthog_key", keyError);
14171
+ else pending.push({ name: "POSTHOG_KEY", value: v || null });
13905
14172
  }
13906
14173
  if ("posthog_host" in body) {
13907
14174
  const v = String(body.posthog_host ?? "").trim().replace(/\/$/, "");
@@ -14666,7 +14933,7 @@ function createApp() {
14666
14933
  const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
14667
14934
  const isDeletionRequestAction = /\/request-deletion$/.test(path) || /\/deletion-requests\/[^/]+\/withdraw$/.test(path);
14668
14935
  const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
14669
- if (isStewardRoster && permission !== "owner" && permission !== "admin") {
14936
+ if (isStewardRoster && access.rawPermission !== "owner" && access.rawPermission !== "admin") {
14670
14937
  return c.json(
14671
14938
  {
14672
14939
  error: "You don't have permission to manage stewards. Only a nest admin, the nest owner, or the server admin can do this."
@@ -14674,6 +14941,13 @@ function createApp() {
14674
14941
  403
14675
14942
  );
14676
14943
  }
14944
+ if (isStewardRoster && access.archived && c.req.method !== "GET") {
14945
+ return deny(
14946
+ 403,
14947
+ "This nest is archived, so it is read-only. Restore it to change stewards.",
14948
+ { archived: true }
14949
+ );
14950
+ }
14677
14951
  const resource = parts[1];
14678
14952
  if (resource === "visibility" || resource === "reader-mode") {
14679
14953
  required = "admin";
@@ -14712,6 +14986,13 @@ function createApp() {
14712
14986
  const grantLevel = grantRole ? permissionLevel(grantRole) : grantNestVisible ? permissionLevel("read") : 0;
14713
14987
  const effectiveLevel = Math.max(permissionLevel(permission), grantLevel);
14714
14988
  if (!stewardEditorBypass && effectiveLevel < permissionLevel(required)) {
14989
+ if (access.archived && permissionLevel(access.rawPermission) >= permissionLevel(required)) {
14990
+ return deny(
14991
+ 403,
14992
+ "This nest is archived, so it is read-only. Restore it to make changes.",
14993
+ { archived: true }
14994
+ );
14995
+ }
14715
14996
  return deny(
14716
14997
  403,
14717
14998
  `You don't have access to perform this action on this nest. Required permission: '${required}', your permission: '${permission}'. Ask the nest owner or a server admin to grant you ${required} access.`,