@promptowl/contextnest-community 1.19.0 → 1.21.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 (83) hide show
  1. package/CONFIGURATION.md +83 -1
  2. package/README.md +1 -1
  3. package/dist/{chunk-FBDEESNJ.js → chunk-4KJGQHWB.js} +113 -218
  4. package/dist/{chunk-ENDHMQPD.js → chunk-6JGQX4GA.js} +415 -64
  5. package/dist/{chunk-TNAZJYL5.js → chunk-6PJQSKKV.js} +12 -4
  6. package/dist/{chunk-BXUVIAMP.js → chunk-IGPJ74O4.js} +60 -12
  7. package/dist/{chunk-HYSTFMFG.js → chunk-LPPKPEYI.js} +131 -1
  8. package/dist/{chunk-3GMGLYTZ.js → chunk-N2DTJFCD.js} +2 -2
  9. package/dist/{chunk-GSYMJ3A4.js → chunk-XWGEXQU3.js} +6 -2
  10. package/dist/{client-SW5I6ZBK.js → client-BD6PAUCZ.js} +1 -1
  11. package/dist/{engine-V4DY4PYN.js → engine-IKZQ46P7.js} +2 -2
  12. package/dist/{external-edit-service-3NK5DGLA.js → external-edit-service-LTON57HO.js} +3 -3
  13. package/dist/{grants-service-CADI6LIB.js → grants-service-LSNZN6NG.js} +2 -2
  14. package/dist/index.js +2704 -1156
  15. package/dist/{migrations.postgres-5VI4RDTS.js → migrations.postgres-JA2YGLS3.js} +66 -1
  16. package/dist/{review-service-LK4M2C43.js → review-service-6JK64IRT.js} +8 -6
  17. package/dist/{stewardship-service-JAFSU5GN.js → stewardship-service-2YKGBP63.js} +3 -3
  18. package/dist/{version-service-G7SFFKFO.js → version-service-PJV74PVQ.js} +11 -3
  19. package/dist/web3/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  20. package/dist/web3/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  21. package/dist/web3/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  22. package/dist/web3/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  23. package/dist/web3/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  24. package/dist/web3/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  25. package/dist/web3/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  26. package/dist/web3/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  27. package/dist/web3/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  28. package/dist/web3/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  29. package/dist/web3/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  30. package/dist/web3/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  31. package/dist/web3/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  32. package/dist/web3/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  33. package/dist/web3/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  34. package/dist/web3/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  35. package/dist/web3/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  36. package/dist/web3/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  37. package/dist/web3/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  38. package/dist/web3/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  39. package/dist/web3/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  40. package/dist/web3/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  41. package/dist/web3/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  42. package/dist/web3/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  43. package/dist/web3/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  44. package/dist/web3/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  45. package/dist/web3/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  46. package/dist/web3/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  47. package/dist/web3/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  48. package/dist/web3/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  49. package/dist/web3/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  50. package/dist/web3/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  51. package/dist/web3/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  52. package/dist/web3/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  53. package/dist/web3/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  54. package/dist/web3/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  55. package/dist/web3/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  56. package/dist/web3/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  57. package/dist/web3/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  58. package/dist/web3/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  59. package/dist/web3/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  60. package/dist/web3/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  61. package/dist/web3/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  62. package/dist/web3/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  63. package/dist/web3/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  64. package/dist/web3/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  65. package/dist/web3/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  66. package/dist/web3/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  67. package/dist/web3/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  68. package/dist/web3/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  69. package/dist/web3/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  70. package/dist/web3/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  71. package/dist/web3/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  72. package/dist/web3/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  73. package/dist/web3/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  74. package/dist/web3/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  75. package/dist/web3/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  76. package/dist/web3/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  77. package/dist/web3/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  78. package/dist/web3/assets/index-T4pilFNL.css +1 -0
  79. package/dist/web3/assets/index-deyhV93k.js +1382 -0
  80. package/dist/web3/index.html +2 -2
  81. package/package.json +6 -1
  82. package/dist/web3/assets/index-C26nM5Kn.css +0 -1
  83. package/dist/web3/assets/index-C4oalc2K.js +0 -1065
@@ -8,7 +8,7 @@ import {
8
8
  config,
9
9
  getDb,
10
10
  isEmailish
11
- } from "./chunk-HYSTFMFG.js";
11
+ } from "./chunk-LPPKPEYI.js";
12
12
  import {
13
13
  ANON_USER_ID
14
14
  } from "./chunk-YB3LKF7U.js";
@@ -27,7 +27,7 @@ import { createEngineApi } from "@promptowl/contextnest-engine/api";
27
27
 
28
28
  // src/nests/service.ts
29
29
  import { mkdirSync } from "fs";
30
- import { v4 as uuid2 } from "uuid";
30
+ import { v4 as uuid3 } from "uuid";
31
31
  import { NestStorage } from "@promptowl/contextnest-engine";
32
32
 
33
33
  // src/shared/paths.ts
@@ -590,7 +590,7 @@ function parseAccessYaml(content) {
590
590
  }
591
591
 
592
592
  // src/governance/teams-service.ts
593
- import { v4 as uuid } from "uuid";
593
+ import { v4 as uuid2 } from "uuid";
594
594
 
595
595
  // src/governance/roles.ts
596
596
  function collabPermToRole(permission) {
@@ -680,6 +680,14 @@ var STATUS_META = {
680
680
  subjectWord: "deleted",
681
681
  label: "Deleted",
682
682
  color: "#dc2626"
683
+ },
684
+ // An honoured ARCHIVE request. Deliberately not the deletion palette: the
685
+ // nest is intact and restorable, and red "Deleted" would tell the requester
686
+ // the opposite of what happened.
687
+ deletion_archived: {
688
+ subjectWord: "archived",
689
+ label: "Archived",
690
+ color: "#0891b2"
683
691
  }
684
692
  };
685
693
  var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
@@ -777,6 +785,14 @@ var TEMPLATES = {
777
785
  lineH: (x) => `${x.doc} has been deleted${x.byH}${x.noteH} \u2014 your deletion request was accepted.`,
778
786
  rows: governanceRows
779
787
  },
788
+ deletion_archived: {
789
+ heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
790
+ detailsHeader: "Nest details",
791
+ linkLabel: "Open nest",
792
+ lineT: (x) => `${x.title} has been archived${x.byT}${x.noteT} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
793
+ lineH: (x) => `${x.doc} has been archived${x.byH}${x.noteH} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
794
+ rows: governanceRows
795
+ },
780
796
  steward: {
781
797
  heading: (x) => `You're now a steward of ${x.title}`,
782
798
  detailsHeader: "Details",
@@ -997,6 +1013,9 @@ async function notifyEmailForNest(nestId, message, details) {
997
1013
  }
998
1014
  }
999
1015
  }
1016
+ function isEmailConfigured() {
1017
+ return !!config.SMTP_URL && !!config.NOTIFY_EMAIL_FROM;
1018
+ }
1000
1019
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1001
1020
  function recipientEnvelope(to, from) {
1002
1021
  const recipients = Array.isArray(to) ? to : [to];
@@ -1118,6 +1137,174 @@ async function buildDocContext(nestId, nodeId, baseUrl) {
1118
1137
  };
1119
1138
  }
1120
1139
 
1140
+ // src/governance/notify-service.ts
1141
+ import { v4 as uuid } from "uuid";
1142
+ async function listWatchers(nestId, nodeId) {
1143
+ return await getDb().all(
1144
+ `SELECT id, node_id, user_email, created_by, created_at
1145
+ FROM watchers WHERE nest_id = ? AND node_id = ? ORDER BY created_at`,
1146
+ [nestId, nodeId]
1147
+ );
1148
+ }
1149
+ async function addWatcher(nestId, nodeId, userEmail, createdBy) {
1150
+ await getDb().run(
1151
+ `INSERT INTO watchers (id, nest_id, node_id, user_email, created_by, created_at)
1152
+ VALUES (?, ?, ?, ?, ?, ?)
1153
+ ON CONFLICT(nest_id, node_id, user_email) DO NOTHING`,
1154
+ [uuid(), nestId, nodeId, userEmail.toLowerCase(), createdBy, (/* @__PURE__ */ new Date()).toISOString()]
1155
+ );
1156
+ }
1157
+ async function removeWatcher(nestId, nodeId, userEmail) {
1158
+ const db = getDb();
1159
+ const row = await db.get(
1160
+ "SELECT id FROM watchers WHERE nest_id = ? AND node_id = ? AND LOWER(user_email) = LOWER(?)",
1161
+ [nestId, nodeId, userEmail]
1162
+ );
1163
+ if (!row) return false;
1164
+ await db.run("DELETE FROM watchers WHERE id = ?", [row.id]);
1165
+ return true;
1166
+ }
1167
+ async function notifyReviewRequested(params) {
1168
+ const { nestId, nodeId, requestedBy, reviewId } = params;
1169
+ const db = getDb();
1170
+ try {
1171
+ const rows = await db.all(
1172
+ `SELECT DISTINCT w.user_email, w.node_id AS via
1173
+ FROM watchers w
1174
+ WHERE w.nest_id = ?
1175
+ AND (w.node_id = ?
1176
+ OR w.node_id IN (
1177
+ SELECT e.from_node FROM edges e
1178
+ JOIN edge_types t ON t.id = e.type_id
1179
+ WHERE e.nest_id = ? AND e.to_node = ? AND t.is_flow = 1
1180
+ ))`,
1181
+ [nestId, nodeId, nestId, nodeId]
1182
+ );
1183
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1184
+ const label = await describeNode(nestId, nodeId);
1185
+ let created = 0;
1186
+ for (const r of rows) {
1187
+ if (r.user_email.toLowerCase() === requestedBy.toLowerCase()) continue;
1188
+ await db.run(
1189
+ `INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
1190
+ VALUES (?, ?, ?, 'review_requested', ?, ?, ?)`,
1191
+ [
1192
+ uuid(),
1193
+ nestId,
1194
+ r.user_email.toLowerCase(),
1195
+ reviewId,
1196
+ r.via === nodeId ? `"${label}" has a new version awaiting review (submitted by ${requestedBy})` : `Agent "${await describeNode(nestId, r.via)}" produced output on "${label}" \u2014 awaiting review`,
1197
+ now
1198
+ ]
1199
+ );
1200
+ created++;
1201
+ }
1202
+ return created;
1203
+ } catch (err) {
1204
+ console.error("[notify] review fan-out failed", nestId, nodeId, err);
1205
+ return 0;
1206
+ }
1207
+ }
1208
+ async function notifyReviewResolved(params) {
1209
+ const { nestId, nodeId, status, resolvedBy, requestedBy, reviewId } = params;
1210
+ const db = getDb();
1211
+ try {
1212
+ const rows = await db.all(
1213
+ `SELECT DISTINCT w.user_email
1214
+ FROM watchers w
1215
+ WHERE w.nest_id = ?
1216
+ AND (w.node_id = ?
1217
+ OR w.node_id IN (
1218
+ SELECT e.from_node FROM edges e
1219
+ JOIN edge_types t ON t.id = e.type_id
1220
+ WHERE e.nest_id = ? AND e.to_node = ? AND t.is_flow = 1
1221
+ ))`,
1222
+ [nestId, nodeId, nestId, nodeId]
1223
+ );
1224
+ const recipients = new Set(rows.map((r) => r.user_email.toLowerCase()));
1225
+ recipients.add(requestedBy.toLowerCase());
1226
+ recipients.delete(resolvedBy.toLowerCase());
1227
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1228
+ const label = await describeNode(nestId, nodeId);
1229
+ let created = 0;
1230
+ for (const email of recipients) {
1231
+ await db.run(
1232
+ `INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
1233
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1234
+ [
1235
+ uuid(),
1236
+ nestId,
1237
+ email,
1238
+ `review_${status}`,
1239
+ reviewId,
1240
+ `"${label}" was ${status} by ${resolvedBy}`,
1241
+ now
1242
+ ]
1243
+ );
1244
+ created++;
1245
+ }
1246
+ return created;
1247
+ } catch (err) {
1248
+ console.error("[notify] resolution fan-out failed", nestId, nodeId, err);
1249
+ return 0;
1250
+ }
1251
+ }
1252
+ async function notifyNestInvite(params) {
1253
+ const { nestId, inviteeEmail, permission, nestName: nestName2, viaTeam } = params;
1254
+ const message = viaTeam ? `Your team "${viaTeam}" was given access to "${nestName2}" as ${permissionLabel(permission)}` : `You were added to "${nestName2}" as ${permissionLabel(permission)}`;
1255
+ try {
1256
+ await getDb().run(
1257
+ `INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
1258
+ VALUES (?, ?, ?, 'nest_invite', ?, ?, ?)`,
1259
+ [
1260
+ uuid(),
1261
+ nestId,
1262
+ inviteeEmail.toLowerCase(),
1263
+ nestId,
1264
+ message,
1265
+ (/* @__PURE__ */ new Date()).toISOString()
1266
+ ]
1267
+ );
1268
+ } catch (err) {
1269
+ console.error("[notify] invite inbox insert failed", nestId, inviteeEmail, err);
1270
+ }
1271
+ }
1272
+ async function listNotifications(userEmail, opts = {}) {
1273
+ const limit = Math.min(opts.limit ?? 50, 200);
1274
+ return await getDb().all(
1275
+ `SELECT n.id, n.nest_id, n.kind, n.subject_id, n.message, n.created_at, n.read_at,
1276
+ COALESCE(
1277
+ rr.node_id,
1278
+ CASE WHEN dr.target_type = 'document' THEN dr.node_id END,
1279
+ -- A mention names the document directly; there's no request row.
1280
+ CASE WHEN n.kind = 'mention' THEN n.subject_id END
1281
+ ) AS node_id
1282
+ FROM notifications n
1283
+ LEFT JOIN review_requests rr ON rr.id = n.subject_id
1284
+ LEFT JOIN deletion_requests dr ON dr.id = n.subject_id
1285
+ WHERE LOWER(n.user_email) = LOWER(?)${opts.unreadOnly ? " AND n.read_at IS NULL" : ""}
1286
+ ORDER BY n.created_at DESC LIMIT ${limit}`,
1287
+ [userEmail]
1288
+ );
1289
+ }
1290
+ async function markNotificationsRead(userEmail, ids) {
1291
+ const db = getDb();
1292
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1293
+ if (ids === "all") {
1294
+ await db.run(
1295
+ "UPDATE notifications SET read_at = ? WHERE LOWER(user_email) = LOWER(?) AND read_at IS NULL",
1296
+ [now, userEmail]
1297
+ );
1298
+ return;
1299
+ }
1300
+ for (const id of ids.slice(0, 200)) {
1301
+ await db.run(
1302
+ "UPDATE notifications SET read_at = ? WHERE id = ? AND LOWER(user_email) = LOWER(?)",
1303
+ [now, id, userEmail]
1304
+ );
1305
+ }
1306
+ }
1307
+
1121
1308
  // src/governance/teams-service.ts
1122
1309
  var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
1123
1310
  function parseMembers(json) {
@@ -1196,7 +1383,7 @@ async function createTeam(params) {
1196
1383
  if (!name) throw new ValidationError("Team name is required");
1197
1384
  const db = getDb();
1198
1385
  const now = (/* @__PURE__ */ new Date()).toISOString();
1199
- const id = uuid();
1386
+ const id = uuid2();
1200
1387
  await db.run(
1201
1388
  "INSERT INTO teams (id, name, owner_id, members, source, external_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1202
1389
  [
@@ -1316,10 +1503,10 @@ async function resolveMemberIdentity(params) {
1316
1503
  };
1317
1504
  }
1318
1505
  const { hashPassword } = await import("./keys-CTOGAG3W.js");
1319
- const userId = uuid();
1506
+ const userId = uuid2();
1320
1507
  await db.run(
1321
1508
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1322
- [userId, requested, null, await hashPassword(uuid())]
1509
+ [userId, requested, null, await hashPassword(uuid2())]
1323
1510
  );
1324
1511
  return { userId, email: requested };
1325
1512
  }
@@ -1397,6 +1584,23 @@ function parseSharedTeams(json) {
1397
1584
  return [];
1398
1585
  }
1399
1586
  }
1587
+ async function listSharedTeamMembers(refs) {
1588
+ const ids = [...new Set(refs.map((r) => r.teamId).filter(Boolean))];
1589
+ if (ids.length === 0) return [];
1590
+ const rows = await getDb().all(
1591
+ `SELECT members FROM teams WHERE id IN (${ids.map(() => "?").join(",")})`,
1592
+ ids
1593
+ );
1594
+ const byEmail = /* @__PURE__ */ new Map();
1595
+ for (const row of rows) {
1596
+ for (const member of parseMembers(row.members)) {
1597
+ const key = (member.email || "").toLowerCase();
1598
+ if (!key || byEmail.has(key)) continue;
1599
+ byEmail.set(key, { userId: member.userId, email: member.email, name: null });
1600
+ }
1601
+ }
1602
+ return [...byEmail.values()];
1603
+ }
1400
1604
  async function listNestTeams(nestId) {
1401
1605
  const db = getDb();
1402
1606
  const row = await db.get("SELECT shared_teams FROM nests WHERE id = ?", [
@@ -1408,6 +1612,7 @@ async function listNestTeams(nestId) {
1408
1612
  async function shareTeamWithNest(params) {
1409
1613
  const db = getDb();
1410
1614
  let sharedMembers = [];
1615
+ let sharedTeamName = "";
1411
1616
  const refs = await db.transaction(async (tx) => {
1412
1617
  const team = await tx.get(
1413
1618
  "SELECT id, name, owner_id, members FROM teams WHERE id = ?",
@@ -1434,6 +1639,7 @@ async function shareTeamWithNest(params) {
1434
1639
  ]);
1435
1640
  const members = parseMembers(team.members);
1436
1641
  sharedMembers = members;
1642
+ sharedTeamName = team.name;
1437
1643
  const hasGovernanceRole = members.some(
1438
1644
  (m) => m.role === "viewer" || m.role === "editor"
1439
1645
  );
@@ -1468,6 +1674,16 @@ async function shareTeamWithNest(params) {
1468
1674
  permission: role,
1469
1675
  by: caller?.email || void 0
1470
1676
  });
1677
+ const permission = teamRoleToPermission(role) ?? role;
1678
+ for (const email of emails) {
1679
+ await notifyNestInvite({
1680
+ nestId: params.nestId,
1681
+ inviteeEmail: email,
1682
+ permission,
1683
+ nestName: shareNestName,
1684
+ viaTeam: sharedTeamName
1685
+ });
1686
+ }
1471
1687
  }
1472
1688
  } catch (err) {
1473
1689
  console.error("[notify] team-share email fan-out failed", params.nestId, err);
@@ -1569,9 +1785,29 @@ var PERMISSION_LEVELS = {
1569
1785
  };
1570
1786
  var higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1571
1787
  async function resolveNestAccess(nestId, userId) {
1788
+ const access = await resolveNestAccessRaw(nestId, userId);
1789
+ return {
1790
+ ...access,
1791
+ rawPermission: access.permission,
1792
+ permission: capForArchive(access.permission, access.archived)
1793
+ };
1794
+ }
1795
+ async function isNestArchived(nestId) {
1796
+ const row = await getDb().get(
1797
+ "SELECT archived_at FROM nests WHERE id = ?",
1798
+ [nestId]
1799
+ );
1800
+ return !!row?.archived_at;
1801
+ }
1802
+ function capForArchive(permission, archived) {
1803
+ if (!archived) return permission;
1804
+ return PERMISSION_LEVELS[permission] > PERMISSION_LEVELS.read ? "read" : permission;
1805
+ }
1806
+ async function resolveNestAccessRaw(nestId, userId) {
1572
1807
  const row = await getDb().get(
1573
1808
  `SELECT n.user_id AS owner_id,
1574
1809
  n.visibility AS visibility,
1810
+ n.archived_at AS archived_at,
1575
1811
  n.shared_teams AS shared_teams,
1576
1812
  u.email AS caller_email,
1577
1813
  nc.permission AS collab_permission,
@@ -1598,7 +1834,9 @@ async function resolveNestAccess(nestId, userId) {
1598
1834
  isSteward: false,
1599
1835
  hasGrant: false,
1600
1836
  permission: "none",
1601
- isPublicReader: false
1837
+ rawPermission: "none",
1838
+ isPublicReader: false,
1839
+ archived: false
1602
1840
  };
1603
1841
  if (!row) return absent;
1604
1842
  const base = {
@@ -1610,7 +1848,8 @@ async function resolveNestAccess(nestId, userId) {
1610
1848
  // Postgres returns integer 1, SQLite returns 1 — both truthy, neither a
1611
1849
  // boolean, so normalize rather than leaking the driver's shape.
1612
1850
  isSteward: !!row.steward_hit,
1613
- hasGrant: !!row.grant_hit
1851
+ hasGrant: !!row.grant_hit,
1852
+ archived: !!row.archived_at
1614
1853
  };
1615
1854
  if (row.owner_id === userId) {
1616
1855
  return { ...base, permission: "owner", isPublicReader: false };
@@ -1696,9 +1935,32 @@ async function isImportedNest(nestId) {
1696
1935
  );
1697
1936
  return !!row?.is_imported;
1698
1937
  }
1938
+ var MAX_NEST_NAME_LENGTH = 100;
1939
+ function plainName(name, emptyMessage = "Name cannot be empty") {
1940
+ const typed = (name ?? "").trim();
1941
+ if (!typed) throw new ValidationError(emptyMessage);
1942
+ if (typed.length > MAX_NEST_NAME_LENGTH) {
1943
+ throw new ValidationError(
1944
+ `Nest names are capped at ${MAX_NEST_NAME_LENGTH} characters`
1945
+ );
1946
+ }
1947
+ if (/<\/?[a-zA-Z][^>]*>/.test(typed)) {
1948
+ throw new ValidationError(
1949
+ "Nest names are plain text \u2014 remove the HTML tags."
1950
+ );
1951
+ }
1952
+ return typed;
1953
+ }
1699
1954
  function toSlug(name) {
1700
1955
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1701
1956
  }
1957
+ async function assertSlugFree(userId, slug, message, exceptId) {
1958
+ const conflict = await getDb().get(
1959
+ exceptId ? "SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1" : "SELECT id FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
1960
+ exceptId ? [userId, slug, exceptId] : [userId, slug]
1961
+ );
1962
+ if (conflict) throw new ValidationError(message);
1963
+ }
1702
1964
  async function uniqueNestName(userId, baseName) {
1703
1965
  const db = getDb();
1704
1966
  let candidate = baseName;
@@ -1730,6 +1992,22 @@ async function setStewardshipEnabled(nestId, enabled) {
1730
1992
  nestId
1731
1993
  ]);
1732
1994
  }
1995
+ async function setReaderMode(nestId, opts) {
1996
+ const db = getDb();
1997
+ const sets = [];
1998
+ const params = [];
1999
+ if (opts.enabled !== void 0) {
2000
+ sets.push("reader_mode = ?");
2001
+ params.push(opts.enabled ? 1 : 0);
2002
+ }
2003
+ if (opts.homeNode !== void 0) {
2004
+ sets.push("reader_home_node = ?");
2005
+ params.push(opts.homeNode ?? null);
2006
+ }
2007
+ if (sets.length === 0) return;
2008
+ params.push(nestId);
2009
+ await db.run(`UPDATE nests SET ${sets.join(", ")} WHERE id = ?`, params);
2010
+ }
1733
2011
  async function nestAllowsSelfApprove(nestId) {
1734
2012
  const db = getDb();
1735
2013
  const row = await db.get(
@@ -1806,19 +2084,15 @@ async function renameNest(nestId, patch) {
1806
2084
  const updates = [];
1807
2085
  const params = [];
1808
2086
  if (patch.name !== void 0) {
1809
- const trimmed = patch.name.trim();
1810
- if (!trimmed) throw new ValidationError("Name cannot be empty");
2087
+ const trimmed = plainName(patch.name);
1811
2088
  const slug = toSlug(trimmed);
1812
2089
  if (!slug) throw new ValidationError("Name must contain at least one alphanumeric character");
1813
- const conflict = await db.get(
1814
- "SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1",
1815
- [existing.user_id, slug, nestId]
2090
+ await assertSlugFree(
2091
+ existing.user_id,
2092
+ slug,
2093
+ "Another nest already uses this name. Pick a different one.",
2094
+ nestId
1816
2095
  );
1817
- if (conflict) {
1818
- throw new ValidationError(
1819
- `Another nest already uses this name. Pick a different one.`
1820
- );
1821
- }
1822
2096
  updates.push("name = ?", "slug = ?");
1823
2097
  params.push(trimmed, slug);
1824
2098
  }
@@ -1837,25 +2111,20 @@ async function renameNest(nestId, patch) {
1837
2111
  return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]);
1838
2112
  }
1839
2113
  async function createNest(userId, name, description) {
1840
- const id = uuid2();
2114
+ const id = uuid3();
1841
2115
  const db = getDb();
1842
- const trimmed = name.trim();
1843
- if (!trimmed) throw new ValidationError("Name cannot be empty");
2116
+ const trimmed = plainName(name);
1844
2117
  const slug = toSlug(trimmed);
1845
2118
  if (!slug) {
1846
2119
  throw new ValidationError(
1847
2120
  "Name must contain at least one alphanumeric character"
1848
2121
  );
1849
2122
  }
1850
- const conflict = await db.get(
1851
- "SELECT id FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
1852
- [userId, slug]
2123
+ await assertSlugFree(
2124
+ userId,
2125
+ slug,
2126
+ "You already have a nest with this name. Pick a different one."
1853
2127
  );
1854
- if (conflict) {
1855
- throw new ValidationError(
1856
- "You already have a nest with this name. Pick a different one."
1857
- );
1858
- }
1859
2128
  const visibility = userId === ANON_USER_ID ? "public" : "private";
1860
2129
  await db.run(
1861
2130
  "INSERT INTO nests (id, user_id, name, slug, description, visibility) VALUES (?, ?, ?, ?, ?, ?)",
@@ -1869,13 +2138,15 @@ async function createNest(userId, name, description) {
1869
2138
  return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1870
2139
  }
1871
2140
  async function importNest(userId, name) {
1872
- const nm = (name || "").trim();
1873
- if (!nm) {
1874
- throw new ValidationError("name is required");
1875
- }
2141
+ const nm = plainName(name || "", "name is required");
1876
2142
  const db = getDb();
1877
- const id = uuid2();
2143
+ const id = uuid3();
1878
2144
  const slug = toSlug(nm);
2145
+ await assertSlugFree(
2146
+ userId,
2147
+ slug,
2148
+ "A nest with this name already exists. Pick a different name."
2149
+ );
1879
2150
  const visibility = userId === ANON_USER_ID ? "public" : "private";
1880
2151
  mkdirSync(resolveNestPath(id), { recursive: true });
1881
2152
  await db.run(
@@ -1885,26 +2156,79 @@ async function importNest(userId, name) {
1885
2156
  trackEvent("nest.import", { nestId: id, userId });
1886
2157
  return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1887
2158
  }
1888
- async function listNests(userId) {
2159
+ async function pinNest(userId, nestId) {
1889
2160
  const db = getDb();
2161
+ const existing = await db.get(
2162
+ "SELECT 1 FROM nest_pins WHERE user_id = ? AND nest_id = ?",
2163
+ [userId, nestId]
2164
+ );
2165
+ if (existing) return;
2166
+ try {
2167
+ await db.run(
2168
+ "INSERT INTO nest_pins (user_id, nest_id) VALUES (?, ?)",
2169
+ [userId, nestId]
2170
+ );
2171
+ } catch (err) {
2172
+ const now = await db.get(
2173
+ "SELECT 1 FROM nest_pins WHERE user_id = ? AND nest_id = ?",
2174
+ [userId, nestId]
2175
+ );
2176
+ if (!now) throw err;
2177
+ }
2178
+ }
2179
+ async function unpinNest(userId, nestId) {
2180
+ await getDb().run(
2181
+ "DELETE FROM nest_pins WHERE user_id = ? AND nest_id = ?",
2182
+ [userId, nestId]
2183
+ );
2184
+ }
2185
+ async function pinnedNestIds(userId) {
2186
+ const rows = await getDb().all(
2187
+ "SELECT nest_id FROM nest_pins WHERE user_id = ?",
2188
+ [userId]
2189
+ );
2190
+ return new Set(rows.map((r) => r.nest_id));
2191
+ }
2192
+ var LIVE_ONLY = "archived_at IS NULL";
2193
+ var LIVE_ONLY_N = "n.archived_at IS NULL";
2194
+ async function listNests(userId, opts = {}) {
2195
+ const db = getDb();
2196
+ const scope = opts.archived ? "archived_at IS NOT NULL" : LIVE_ONLY;
1890
2197
  if (userId === ANON_USER_ID) {
1891
2198
  return await db.all(
1892
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
2199
+ `SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY created_at DESC`,
1893
2200
  [ANON_USER_ID]
1894
2201
  );
1895
2202
  }
1896
2203
  const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
1897
2204
  if (!includeAnon) {
1898
2205
  return await db.all(
1899
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
2206
+ `SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY created_at DESC`,
1900
2207
  [userId]
1901
2208
  );
1902
2209
  }
1903
2210
  return await db.all(
1904
- "SELECT * FROM nests WHERE user_id = ? OR user_id = ? ORDER BY created_at DESC",
2211
+ `SELECT * FROM nests WHERE (user_id = ? OR user_id = ?) AND ${scope}
2212
+ ORDER BY created_at DESC`,
1905
2213
  [userId, ANON_USER_ID]
1906
2214
  );
1907
2215
  }
2216
+ async function archiveNest(nestId) {
2217
+ await getDb().transaction(async (tx) => {
2218
+ await tx.run(
2219
+ "UPDATE nests SET archived_at = ? WHERE id = ? AND archived_at IS NULL",
2220
+ [(/* @__PURE__ */ new Date()).toISOString(), nestId]
2221
+ );
2222
+ await tx.run(
2223
+ `DELETE FROM deletion_requests
2224
+ WHERE nest_id = ? AND target_type = 'nest_archive' AND status = 'pending'`,
2225
+ [nestId]
2226
+ );
2227
+ });
2228
+ }
2229
+ async function restoreNest(nestId) {
2230
+ await getDb().run("UPDATE nests SET archived_at = NULL WHERE id = ?", [nestId]);
2231
+ }
1908
2232
  async function nestsSharedViaTeams(userId) {
1909
2233
  const db = getDb();
1910
2234
  const teams = await db.all(
@@ -1926,6 +2250,7 @@ async function nestsSharedViaTeams(userId) {
1926
2250
  const candidates = await db.all(
1927
2251
  `SELECT * FROM nests
1928
2252
  WHERE user_id != ?
2253
+ AND ${LIVE_ONLY}
1929
2254
  AND (${ids.map(() => "shared_teams LIKE ?").join(" OR ")})`,
1930
2255
  [userId, ...ids.map((id) => `%${id}%`)]
1931
2256
  );
@@ -1951,6 +2276,7 @@ async function listSharedNests(userId) {
1951
2276
  LEFT JOIN grants g
1952
2277
  ON g.nest_id = n.id AND g.user_id = ?
1953
2278
  WHERE n.user_id != ?
2279
+ AND ${LIVE_ONLY_N}
1954
2280
  AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL OR g.id IS NOT NULL)
1955
2281
  ORDER BY n.created_at DESC`,
1956
2282
  [userId, userId, userId, userId]
@@ -1964,7 +2290,8 @@ async function listSharedNests(userId) {
1964
2290
  }
1965
2291
  if (await isServerAdminUserId(userId)) {
1966
2292
  const anon = await db.all(
1967
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
2293
+ `SELECT * FROM nests WHERE user_id = ? AND ${LIVE_ONLY}
2294
+ ORDER BY created_at DESC`,
1968
2295
  [ANON_USER_ID]
1969
2296
  );
1970
2297
  for (const n of anon) {
@@ -1998,11 +2325,12 @@ async function listVisibleNests(userId, opts = {}) {
1998
2325
  ON s.nest_id = n.id AND s.is_active = 1
1999
2326
  AND LOWER(s.user_email) = LOWER(u.email)
2000
2327
  ${opts.readable ? "LEFT JOIN grants g ON g.nest_id = n.id AND g.user_id = ?" : ""}
2001
- WHERE n.user_id = ?
2328
+ WHERE ${LIVE_ONLY_N}
2329
+ AND (n.user_id = ?
2002
2330
  OR nc.user_id IS NOT NULL
2003
2331
  OR s.id IS NOT NULL
2004
2332
  ${opts.readable ? "OR g.id IS NOT NULL OR n.visibility = 'public'" : ""}
2005
- ${includeAnon ? "OR n.user_id = ?" : ""}
2333
+ ${includeAnon ? "OR n.user_id = ?" : ""})
2006
2334
  ORDER BY n.created_at DESC`,
2007
2335
  params
2008
2336
  );
@@ -2020,6 +2348,7 @@ async function listPublicNests(userId) {
2020
2348
  return await db.all(
2021
2349
  `SELECT n.* FROM nests n
2022
2350
  WHERE n.visibility = 'public'
2351
+ AND ${LIVE_ONLY_N}
2023
2352
  AND n.user_id != ?
2024
2353
  AND NOT EXISTS (
2025
2354
  SELECT 1 FROM nest_collaborators nc
@@ -2047,6 +2376,10 @@ async function deleteNest(nestId) {
2047
2376
  } catch {
2048
2377
  }
2049
2378
  await tx.run("DELETE FROM api_keys WHERE nest_id = ?", [id]);
2379
+ try {
2380
+ await tx.run("DELETE FROM nest_pins WHERE nest_id = ?", [id]);
2381
+ } catch {
2382
+ }
2050
2383
  await tx.run("DELETE FROM nests WHERE id = ?", [id]);
2051
2384
  });
2052
2385
  const path = resolveNestPath(nestId);
@@ -2068,6 +2401,21 @@ function isSkippedSegment(segment) {
2068
2401
  return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
2069
2402
  }
2070
2403
 
2404
+ // src/nodes/meta-files.ts
2405
+ var META_BASENAMES = /* @__PURE__ */ new Set([
2406
+ "INDEX",
2407
+ "CONTEXT",
2408
+ "CLAUDE",
2409
+ "GEMINI",
2410
+ "AGENTS",
2411
+ "QWEN",
2412
+ "README"
2413
+ ]);
2414
+ function isMetaFile(idOrPath) {
2415
+ const base = idOrPath.split(/[/\\]/).pop() ?? "";
2416
+ return META_BASENAMES.has(base.replace(/\.md$/i, "").toUpperCase());
2417
+ }
2418
+
2071
2419
  // src/shared/batch.ts
2072
2420
  var IO_CONCURRENCY = 32;
2073
2421
  async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
@@ -2079,14 +2427,6 @@ async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
2079
2427
  }
2080
2428
 
2081
2429
  // src/nodes/flat-storage.ts
2082
- var META_FILES = /* @__PURE__ */ new Set([
2083
- "INDEX.md",
2084
- "CONTEXT.md",
2085
- "CLAUDE.md",
2086
- "GEMINI.md",
2087
- "AGENTS.md",
2088
- "QWEN.md"
2089
- ]);
2090
2430
  var FlatNestStorage = class extends NestStorage2 {
2091
2431
  async discoverDocuments() {
2092
2432
  const root = this.root;
@@ -2100,7 +2440,7 @@ var FlatNestStorage = class extends NestStorage2 {
2100
2440
  const ids = [];
2101
2441
  for (const e of entries) {
2102
2442
  if (!e.isFile() || !e.name.endsWith(".md")) continue;
2103
- if (META_FILES.has(e.name)) continue;
2443
+ if (isMetaFile(e.name)) continue;
2104
2444
  const dir = e.parentPath ?? e.path ?? root;
2105
2445
  const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
2106
2446
  if (rel.split("/").some(isSkippedSegment)) continue;
@@ -2247,8 +2587,8 @@ async function ensureNodeIndex(nestId) {
2247
2587
  return run;
2248
2588
  }
2249
2589
  async function rebuildNodeIndex(nestId) {
2250
- const { engineCache: engineCache2 } = await import("./engine-V4DY4PYN.js");
2251
- const { documentsWithSuggestions } = await import("./external-edit-service-3NK5DGLA.js");
2590
+ const { engineCache: engineCache2 } = await import("./engine-IKZQ46P7.js");
2591
+ const { documentsWithSuggestions } = await import("./external-edit-service-LTON57HO.js");
2252
2592
  const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
2253
2593
  dropDiscoveryCache();
2254
2594
  const startedAt = writeGeneration.get(nestId) ?? 0;
@@ -2345,18 +2685,11 @@ async function indexedFolderCounts(nestId) {
2345
2685
  // src/nodes/engine.ts
2346
2686
  var DISCOVERY_TTL_MS = 3e4;
2347
2687
  var isLive = (node) => node.frontmatter.status !== "rejected";
2348
- var DERIVED_VAULT_FILES = /* @__PURE__ */ new Set([
2349
- "INDEX.md",
2350
- "CONTEXT.md",
2351
- "CLAUDE.md",
2352
- "GEMINI.md",
2353
- "AGENTS.md",
2354
- "README.md"
2355
- ]);
2356
2688
  function withDiscoveryCache(nestId, storage) {
2357
2689
  const entries = /* @__PURE__ */ new Map();
2358
2690
  let inflight = null;
2359
- const discover = storage.discoverDocuments.bind(storage);
2691
+ const rawDiscover = storage.discoverDocuments.bind(storage);
2692
+ const discover = async (options) => (await rawDiscover(options)).filter((d) => !isMetaFile(d.id));
2360
2693
  const readDoc = storage.readDocument.bind(storage);
2361
2694
  const filtersRetired = !(storage instanceof FlatNestStorage);
2362
2695
  let generation = 0;
@@ -2407,6 +2740,7 @@ function withDiscoveryCache(nestId, storage) {
2407
2740
  await markIndexStale(nestId);
2408
2741
  };
2409
2742
  const patch = async (id) => {
2743
+ if (isMetaFile(id)) return;
2410
2744
  generation++;
2411
2745
  inflight = null;
2412
2746
  let node = null;
@@ -2448,8 +2782,7 @@ function withDiscoveryCache(nestId, storage) {
2448
2782
  wrap("init", () => desync());
2449
2783
  wrap("writeVaultFile", (args) => {
2450
2784
  const rel = String(args[0] ?? "");
2451
- const base = rel.split(/[/\\]/).pop() || "";
2452
- if (rel.endsWith(".md") && !DERIVED_VAULT_FILES.has(base)) return desync();
2785
+ if (rel.endsWith(".md") && !isMetaFile(rel)) return desync();
2453
2786
  });
2454
2787
  return { storage, invalidate, desync: () => void desync() };
2455
2788
  }
@@ -2513,9 +2846,9 @@ export {
2513
2846
  canEditWith,
2514
2847
  canApproveWith,
2515
2848
  primaryRole,
2516
- permissionLabel,
2517
2849
  verifySmtp,
2518
2850
  notifyEmailForNest,
2851
+ isEmailConfigured,
2519
2852
  sendEmailToRecipient,
2520
2853
  nestStorageRoot,
2521
2854
  resolveNestPath,
@@ -2525,6 +2858,7 @@ export {
2525
2858
  uniqueNestName,
2526
2859
  isStewardshipEnabled,
2527
2860
  setStewardshipEnabled,
2861
+ setReaderMode,
2528
2862
  nestAllowsSelfApprove,
2529
2863
  setAllowSelfApprove,
2530
2864
  nestReviewsPrimeOnly,
@@ -2535,7 +2869,12 @@ export {
2535
2869
  renameNest,
2536
2870
  createNest,
2537
2871
  importNest,
2872
+ pinNest,
2873
+ unpinNest,
2874
+ pinnedNestIds,
2538
2875
  listNests,
2876
+ archiveNest,
2877
+ restoreNest,
2539
2878
  listSharedNests,
2540
2879
  listVisibleNests,
2541
2880
  listPublicNests,
@@ -2563,6 +2902,14 @@ export {
2563
2902
  docLink,
2564
2903
  resolveNodeUrl,
2565
2904
  buildDocContext,
2905
+ listWatchers,
2906
+ addWatcher,
2907
+ removeWatcher,
2908
+ notifyReviewRequested,
2909
+ notifyReviewResolved,
2910
+ notifyNestInvite,
2911
+ listNotifications,
2912
+ markNotificationsRead,
2566
2913
  createTeam,
2567
2914
  findTeamByExternalId,
2568
2915
  listTeamsForUser,
@@ -2574,12 +2921,16 @@ export {
2574
2921
  replaceTeamRoster,
2575
2922
  updateMemberRole,
2576
2923
  removeMember,
2924
+ parseSharedTeams,
2925
+ listSharedTeamMembers,
2577
2926
  listNestTeams,
2578
2927
  shareTeamWithNest,
2579
2928
  unshareTeamFromNest,
2580
2929
  resolveTeamRolesForUser,
2581
2930
  isServerAdminUserId,
2582
2931
  resolveNestAccess,
2932
+ isNestArchived,
2933
+ capForArchive,
2583
2934
  resolveNestPermission,
2584
2935
  permissionLevel,
2585
2936
  isPublicReader