@promptowl/contextnest-community 1.22.0 → 1.23.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/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  getPendingReview,
19
19
  getReviewHistory,
20
20
  getReviewQueue,
21
+ hasAnyConnector,
21
22
  isNestTarget,
22
23
  isSafeConnectorUrl,
23
24
  listDeletionRequests,
@@ -37,7 +38,7 @@ import {
37
38
  submitForReview,
38
39
  withNodeWriteLock,
39
40
  withdrawDeletionRequest
40
- } from "./chunk-OQXZ43HG.js";
41
+ } from "./chunk-MLQU4I5J.js";
41
42
  import {
42
43
  canCreateInNest,
43
44
  canManageStewards,
@@ -57,7 +58,7 @@ import {
57
58
  resolveUserRoles,
58
59
  syncFromConfig,
59
60
  updateSteward
60
- } from "./chunk-PX3P4FTU.js";
61
+ } from "./chunk-2YPY2HGZ.js";
61
62
  import {
62
63
  createGrant,
63
64
  deleteGrant,
@@ -66,7 +67,7 @@ import {
66
67
  listGrants,
67
68
  listUserGrants,
68
69
  resolveNodeGrant
69
- } from "./chunk-MY4JIWQD.js";
70
+ } from "./chunk-UOMW7TT6.js";
70
71
  import {
71
72
  generateApiKey,
72
73
  getKeyPrefix,
@@ -92,7 +93,7 @@ import {
92
93
  listMyDrafts,
93
94
  setApprovedVersion,
94
95
  upsertVersion
95
- } from "./chunk-6WESEA75.js";
96
+ } from "./chunk-5CVZMHHB.js";
96
97
  import {
97
98
  approveExternalEdit,
98
99
  getExternalEditDetail,
@@ -104,12 +105,13 @@ import {
104
105
  scanDocumentForDrift,
105
106
  scanNestForDrift,
106
107
  startDriftScanner
107
- } from "./chunk-ZAV4QUZA.js";
108
+ } from "./chunk-XYD6V2LH.js";
108
109
  import {
109
110
  addMember,
110
111
  addWatcher,
111
112
  archiveNest,
112
113
  assertSafeNodeId,
114
+ buildDocContext,
113
115
  buildTitleMap,
114
116
  canApproveWith,
115
117
  canEditWith,
@@ -119,7 +121,6 @@ import {
119
121
  createTeam,
120
122
  deleteNest,
121
123
  deleteTeam,
122
- describeNode,
123
124
  disableStewardshipAndWipeGovernance,
124
125
  docLink,
125
126
  engineApi,
@@ -169,6 +170,7 @@ import {
169
170
  normalizeTag,
170
171
  notifyNestInvite,
171
172
  nowExpr,
173
+ nowMsExpr,
172
174
  opContext,
173
175
  parseSharedTeams,
174
176
  permissionLevel,
@@ -200,6 +202,7 @@ import {
200
202
  shareTeamWithNest,
201
203
  startLicenseSafetyPoll,
202
204
  startTelemetryLoop,
205
+ suffixOf,
203
206
  syncNodeTags,
204
207
  trackEvent,
205
208
  uniqueNestName,
@@ -208,7 +211,7 @@ import {
208
211
  updateMemberRole,
209
212
  validateLicense,
210
213
  verifySmtp
211
- } from "./chunk-W5ILNGPD.js";
214
+ } from "./chunk-QLXC6542.js";
212
215
  import {
213
216
  AppError,
214
217
  ConflictError,
@@ -226,7 +229,7 @@ import {
226
229
  initDb,
227
230
  isEmailListish,
228
231
  isEmailish
229
- } from "./chunk-J2OQ3MEB.js";
232
+ } from "./chunk-QWNXWRWO.js";
230
233
  import {
231
234
  ANON_EMAIL,
232
235
  ANON_USER_ID,
@@ -240,7 +243,7 @@ import {
240
243
  import { serve } from "@hono/node-server";
241
244
 
242
245
  // src/app.ts
243
- import { Hono as Hono23 } from "hono";
246
+ import { Hono as Hono25 } from "hono";
244
247
  import { LinearRouter } from "hono/router/linear-router";
245
248
  import { createMiddleware as createMiddleware2 } from "hono/factory";
246
249
  import { cors } from "hono/cors";
@@ -270,7 +273,7 @@ function applyArtifactSandboxHeaders(c) {
270
273
 
271
274
  // src/auth/routes.ts
272
275
  import { Hono as Hono2 } from "hono";
273
- import { v4 as uuid3 } from "uuid";
276
+ import { v4 as uuid4 } from "uuid";
274
277
 
275
278
  // src/auth/middleware.ts
276
279
  import { createMiddleware } from "hono/factory";
@@ -565,10 +568,89 @@ function verifySsoTicket(token, secret, nowSec = Math.floor(Date.now() / 1e3)) {
565
568
  return claims;
566
569
  }
567
570
 
571
+ // src/auth/community-sites.ts
572
+ import { v4 as uuid } from "uuid";
573
+ function rowToSite(row) {
574
+ return { ...row, is_active: !!row.is_active };
575
+ }
576
+ var SELECT_COLUMNS = "id, name, url, secret, is_active, created_at, updated_at";
577
+ var activeSitesCache = [];
578
+ async function loadActiveCommunitySites() {
579
+ const rows = await getDb().all(
580
+ `SELECT ${SELECT_COLUMNS} FROM community_sites WHERE is_active = 1 ORDER BY name`
581
+ );
582
+ activeSitesCache = rows.map(rowToSite);
583
+ }
584
+ function getActiveCommunitySites() {
585
+ return activeSitesCache;
586
+ }
587
+ async function listCommunitySites() {
588
+ const rows = await getDb().all(
589
+ `SELECT ${SELECT_COLUMNS} FROM community_sites ORDER BY name`
590
+ );
591
+ return rows.map(rowToSite);
592
+ }
593
+ async function getCommunitySite(id) {
594
+ const row = await getDb().get(
595
+ `SELECT ${SELECT_COLUMNS} FROM community_sites WHERE id = ?`,
596
+ [id]
597
+ );
598
+ return row ? rowToSite(row) : null;
599
+ }
600
+ async function createCommunitySite(input) {
601
+ const id = uuid();
602
+ await getDb().run(
603
+ `INSERT INTO community_sites (id, name, url, secret, is_active) VALUES (?, ?, ?, ?, ?)`,
604
+ [id, input.name, input.url, input.secret, input.is_active ? 1 : 0]
605
+ );
606
+ const site = await getCommunitySite(id);
607
+ if (!site) throw new Error("Failed to create community site");
608
+ return site;
609
+ }
610
+ async function updateCommunitySite(id, patch) {
611
+ const existing = await getCommunitySite(id);
612
+ if (!existing) return null;
613
+ const next = {
614
+ name: patch.name ?? existing.name,
615
+ url: patch.url ?? existing.url,
616
+ secret: patch.secret ?? existing.secret,
617
+ is_active: patch.is_active ?? existing.is_active
618
+ };
619
+ const db = getDb();
620
+ await db.run(
621
+ `UPDATE community_sites
622
+ SET name = ?, url = ?, secret = ?, is_active = ?, updated_at = ${nowExpr(db)}
623
+ WHERE id = ?`,
624
+ [next.name, next.url, next.secret, next.is_active ? 1 : 0, id]
625
+ );
626
+ return getCommunitySite(id);
627
+ }
628
+ async function deleteCommunitySite(id) {
629
+ const result = await getDb().run("DELETE FROM community_sites WHERE id = ?", [
630
+ id
631
+ ]);
632
+ return result.changes > 0;
633
+ }
634
+ function legacyCandidate() {
635
+ const secret = config.OFFICIAL_COMMUNITY_SSO_SECRET;
636
+ if (!secret) return null;
637
+ return { id: "legacy", label: "legacy", secret, url: config.PUBLIC_BASE_URL };
638
+ }
639
+ function ssoCandidates() {
640
+ const fromDb = getActiveCommunitySites().map((s) => ({
641
+ id: s.id,
642
+ label: s.name,
643
+ secret: s.secret,
644
+ url: s.url.replace(/\/$/, "")
645
+ }));
646
+ const legacy = legacyCandidate();
647
+ return legacy ? [...fromDb, legacy] : fromDb;
648
+ }
649
+
568
650
  // src/auth/oidc.ts
569
651
  import { Hono } from "hono";
570
652
  import { createHash, randomBytes as randomBytes2 } from "crypto";
571
- import { v4 as uuid } from "uuid";
653
+ import { v4 as uuid2 } from "uuid";
572
654
  import { createRemoteJWKSet, jwtVerify } from "jose";
573
655
 
574
656
  // src/shared/base-url.ts
@@ -824,8 +906,8 @@ oidcRoutes.get("/callback", async (c) => {
824
906
  );
825
907
  if (!user) {
826
908
  if (!config.OIDC_AUTO_PROVISION) return bounce("not_invited");
827
- const userId = uuid();
828
- const placeholderHash = await hashPassword(uuid());
909
+ const userId = uuid2();
910
+ const placeholderHash = await hashPassword(uuid2());
829
911
  try {
830
912
  await db.run(
831
913
  "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
@@ -897,7 +979,7 @@ oidcRoutes.get("/logout", async (c) => {
897
979
  });
898
980
 
899
981
  // src/auth/token-exchange.ts
900
- import { v4 as uuid2 } from "uuid";
982
+ import { v4 as uuid3 } from "uuid";
901
983
  import { jwtVerify as jwtVerify2 } from "jose";
902
984
  var TX_LIMIT = { max: 60, windowMs: 15 * 6e4 };
903
985
  var ExchangeError = class extends Error {
@@ -984,8 +1066,8 @@ async function resolveUser(email, name, department) {
984
1066
  if (!config.OIDC_AUTO_PROVISION || config.OIDC_ALLOWED_DOMAINS.length === 0) {
985
1067
  throw new ExchangeError("no_account", 403);
986
1068
  }
987
- const userId = uuid2();
988
- const placeholderHash = await hashPassword(uuid2());
1069
+ const userId = uuid3();
1070
+ const placeholderHash = await hashPassword(uuid3());
989
1071
  try {
990
1072
  await db.run(
991
1073
  "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
@@ -1161,8 +1243,8 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
1161
1243
  [meEmail]
1162
1244
  );
1163
1245
  if (!user) {
1164
- const userId = uuid3();
1165
- const placeholderHash = await hashPassword(uuid3());
1246
+ const userId = uuid4();
1247
+ const placeholderHash = await hashPassword(uuid4());
1166
1248
  await db.run(
1167
1249
  "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
1168
1250
  [userId, meEmail, rawName || null, placeholderHash]
@@ -1235,7 +1317,7 @@ authRoutes.post("/register", async (c) => {
1235
1317
  }
1236
1318
  throw new ValidationError("Email already registered");
1237
1319
  }
1238
- const userId = uuid3();
1320
+ const userId = uuid4();
1239
1321
  const passwordHash = await hashPassword(body.password);
1240
1322
  await db.run(
1241
1323
  "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
@@ -1337,7 +1419,7 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
1337
1419
  });
1338
1420
  if (!check.ok) return c.json({ error: check.error }, check.status);
1339
1421
  const apiKey = generateApiKey();
1340
- const keyId = uuid3();
1422
+ const keyId = uuid4();
1341
1423
  await db.run(
1342
1424
  "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
1343
1425
  [
@@ -1366,7 +1448,7 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
1366
1448
  if (!check.ok) return c.json({ error: check.error }, check.status);
1367
1449
  const prior = check.key;
1368
1450
  const apiKey = generateApiKey();
1369
- const keyId = uuid3();
1451
+ const keyId = uuid4();
1370
1452
  const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
1371
1453
  const replaced = await db.transaction(async (tx) => {
1372
1454
  const removed = await tx.run(
@@ -1520,25 +1602,30 @@ authRoutes.post("/promptowl", async (c) => {
1520
1602
  });
1521
1603
  authRoutes.get("/sso", async (c) => {
1522
1604
  const ssoError = (code) => c.redirect(`/?sso_error=${code}`, 302);
1523
- const secret = config.OFFICIAL_COMMUNITY_SSO_SECRET;
1524
- if (!secret) return ssoError("not_supported");
1605
+ const candidates = ssoCandidates();
1606
+ if (candidates.length === 0) return ssoError("not_supported");
1525
1607
  if (!tryConsume(`sso:ip:${clientIp(c)}`, DEVICE_LIMIT)) {
1526
1608
  return ssoError("rate_limited");
1527
1609
  }
1528
1610
  const ticket = c.req.query("ticket");
1529
1611
  if (!ticket) return ssoError("missing_ticket");
1530
1612
  let claims;
1531
- try {
1532
- claims = verifySsoTicket(ticket, secret);
1533
- } catch {
1534
- return ssoError("invalid_ticket");
1613
+ let matched;
1614
+ for (const candidate of candidates) {
1615
+ try {
1616
+ claims = verifySsoTicket(ticket, candidate.secret);
1617
+ matched = candidate;
1618
+ break;
1619
+ } catch {
1620
+ }
1535
1621
  }
1622
+ if (!claims || !matched) return ssoError("invalid_ticket");
1536
1623
  if ((claims.scope || "") === "mcp") return ssoError("invalid_ticket");
1537
1624
  const expectedAud = config.PUBLIC_BASE_URL;
1538
1625
  if (!expectedAud && !warnedMissingSsoAudience) {
1539
1626
  warnedMissingSsoAudience = true;
1540
1627
  console.warn(
1541
- "[sso] OFFICIAL_COMMUNITY_SSO_SECRET is set but PUBLIC_BASE_URL is empty \u2014 ticket audience binding is DISABLED. Set PUBLIC_BASE_URL so a ticket minted for another server cannot be replayed against this one."
1628
+ `[sso] site "${matched.label}" has a secret but PUBLIC_BASE_URL is unset \u2014 ticket audience binding is DISABLED. Set PUBLIC_BASE_URL to this deployment's own URL so a ticket minted for another deployment cannot be replayed against this one.`
1542
1629
  );
1543
1630
  }
1544
1631
  if (expectedAud) {
@@ -1674,7 +1761,7 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
1674
1761
  if (supplied !== void 0) {
1675
1762
  assertValidPassword(supplied);
1676
1763
  }
1677
- const generated = supplied ? null : uuid3().replace(/-/g, "").slice(0, 16);
1764
+ const generated = supplied ? null : uuid4().replace(/-/g, "").slice(0, 16);
1678
1765
  const newPassword = supplied ?? generated;
1679
1766
  const newHash = await hashPassword(newPassword);
1680
1767
  await db.transaction(async (tx) => {
@@ -1789,7 +1876,7 @@ authRoutes.post("/invite", async (c) => {
1789
1876
  409
1790
1877
  );
1791
1878
  }
1792
- const tempPassword = uuid3().replace(/-/g, "").slice(0, 16);
1879
+ const tempPassword = uuid4().replace(/-/g, "").slice(0, 16);
1793
1880
  const passwordHash = await hashPassword(tempPassword);
1794
1881
  let userId;
1795
1882
  if (existing) {
@@ -1799,7 +1886,7 @@ authRoutes.post("/invite", async (c) => {
1799
1886
  userId
1800
1887
  ]);
1801
1888
  } else {
1802
- userId = uuid3();
1889
+ userId = uuid4();
1803
1890
  await db.run(
1804
1891
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1805
1892
  [userId, email, null, passwordHash]
@@ -1939,7 +2026,43 @@ function documentPrimeFlag(metadata) {
1939
2026
  }
1940
2027
 
1941
2028
  // src/governance/mention-notify.ts
1942
- import { v4 as uuid4 } from "uuid";
2029
+ import { v4 as uuid5 } from "uuid";
2030
+
2031
+ // src/shared/mentions.ts
2032
+ var MENTION = /(?:^|\s)@(?:\(([^)]+)\)|([a-zA-Z0-9._@-]+))/g;
2033
+ function stripCode(markdown2) {
2034
+ return markdown2.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]*`/g, " ");
2035
+ }
2036
+ function extractMentions(markdown2) {
2037
+ const names = [];
2038
+ const seen = /* @__PURE__ */ new Set();
2039
+ const text = stripCode(markdown2 || "");
2040
+ let match;
2041
+ MENTION.lastIndex = 0;
2042
+ while ((match = MENTION.exec(text)) !== null) {
2043
+ const name = (match[1] || match[2] || "").trim();
2044
+ const key = name.toLowerCase();
2045
+ if (!name || seen.has(key)) continue;
2046
+ seen.add(key);
2047
+ names.push(name);
2048
+ }
2049
+ return names;
2050
+ }
2051
+ function newMentions(previous, next) {
2052
+ const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
2053
+ return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
2054
+ }
2055
+ var MAX_MENTION_RECIPIENTS = 25;
2056
+ function resolveMention(people, name) {
2057
+ const n = name.trim().toLowerCase();
2058
+ if (!n) return void 0;
2059
+ const exact = people.find((p) => p.email.toLowerCase() === n);
2060
+ if (exact) return exact;
2061
+ const matches = people.filter(
2062
+ (p) => p.email.toLowerCase().split("@")[0] === n || (p.name || "").toLowerCase() === n
2063
+ );
2064
+ return matches.length === 1 ? matches[0] : void 0;
2065
+ }
1943
2066
 
1944
2067
  // src/nests/people-service.ts
1945
2068
  var PERSON_SOURCES = [
@@ -2003,40 +2126,129 @@ async function listNestPeople(nestId) {
2003
2126
  }
2004
2127
  return people;
2005
2128
  }
2006
-
2007
- // src/shared/mentions.ts
2008
- var MENTION = /(?:^|\s)@(?:\(([^)]+)\)|([a-zA-Z0-9._@-]+))/g;
2009
- function stripCode(markdown2) {
2010
- return markdown2.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]*`/g, " ");
2011
- }
2012
- function extractMentions(markdown2) {
2013
- const names = [];
2014
- const seen = /* @__PURE__ */ new Set();
2015
- const text = stripCode(markdown2 || "");
2016
- let match;
2017
- MENTION.lastIndex = 0;
2018
- while ((match = MENTION.exec(text)) !== null) {
2019
- const name = (match[1] || match[2] || "").trim();
2020
- const key = name.toLowerCase();
2021
- if (!name || seen.has(key)) continue;
2022
- seen.add(key);
2023
- names.push(name);
2129
+ async function withDirectory(roster) {
2130
+ if (!config.PEOPLE_SUGGEST_ALL_USERS) return roster;
2131
+ const seen = new Set(roster.map((p) => p.email.toLowerCase()));
2132
+ const rows = await getDb().all(
2133
+ "SELECT id, email, name FROM users WHERE id != ? ORDER BY email LIMIT ?",
2134
+ [ANON_USER_ID, SUGGEST_FETCH]
2135
+ );
2136
+ const extra = [];
2137
+ for (const r of rows) {
2138
+ const email = (r.email || "").trim();
2139
+ if (!email || seen.has(email.toLowerCase())) continue;
2140
+ extra.push({ email, name: r.name ?? null, userId: r.id, source: "directory" });
2024
2141
  }
2025
- return names;
2142
+ return [...roster, ...extra];
2026
2143
  }
2027
- function newMentions(previous, next) {
2028
- const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
2029
- return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
2030
- }
2031
- function resolveMention(people, name) {
2144
+ async function findDirectoryPerson(name) {
2145
+ if (!config.PEOPLE_SUGGEST_ALL_USERS) return void 0;
2032
2146
  const n = name.trim().toLowerCase();
2033
2147
  if (!n) return void 0;
2034
- const exact = people.find((p) => p.email.toLowerCase() === n);
2035
- if (exact) return exact;
2036
- const matches = people.filter(
2037
- (p) => p.email.toLowerCase().split("@")[0] === n || (p.name || "").toLowerCase() === n
2038
- );
2039
- return matches.length === 1 ? matches[0] : void 0;
2148
+ const rows = await getDb().all(
2149
+ `SELECT id, email, name FROM users
2150
+ WHERE id != ?
2151
+ AND (LOWER(email) = ? OR LOWER(email) LIKE ? ESCAPE '\\' OR LOWER(COALESCE(name, '')) = ?)`,
2152
+ [ANON_USER_ID, n, `${escapeLike(n)}@%`, n]
2153
+ );
2154
+ return resolveMention(
2155
+ rows.map((r) => ({ email: r.email, name: r.name ?? null, userId: r.id, source: "directory" })),
2156
+ name
2157
+ );
2158
+ }
2159
+ var SUGGEST_LIMIT = 20;
2160
+ var SUGGEST_FETCH = 200;
2161
+ function matchesQuery(person, q) {
2162
+ if (!q) return true;
2163
+ return person.email.toLowerCase().includes(q) || (person.name || "").toLowerCase().includes(q);
2164
+ }
2165
+ var escapeLike = (s) => s.replace(/([\\%_])/g, "\\$1");
2166
+ function queryFilter(q) {
2167
+ if (!q) return { sql: "", args: [] };
2168
+ const pattern = `%${escapeLike(q)}%`;
2169
+ return {
2170
+ sql: " AND (LOWER(u.email) LIKE ? ESCAPE '\\' OR LOWER(COALESCE(u.name, '')) LIKE ? ESCAPE '\\')",
2171
+ args: [pattern, pattern]
2172
+ };
2173
+ }
2174
+ async function suggestPeopleForUser(callerUserId, query = "", limit = SUGGEST_LIMIT) {
2175
+ const db = getDb();
2176
+ const q = query.trim().toLowerCase();
2177
+ const cap = Math.min(Math.max(limit, 1), SUGGEST_LIMIT);
2178
+ const caller = await db.get("SELECT email FROM users WHERE id = ?", [
2179
+ callerUserId
2180
+ ]);
2181
+ const callerEmail = (caller?.email || "").toLowerCase();
2182
+ const filter = queryFilter(q);
2183
+ const wholeDirectory = config.PEOPLE_SUGGEST_ALL_USERS || await isServerAdminUserId(callerUserId);
2184
+ const rows = await (async () => {
2185
+ if (wholeDirectory) {
2186
+ return await db.all(
2187
+ `SELECT u.id AS user_id, u.email AS email, u.name AS name, 'directory' AS source
2188
+ FROM users u
2189
+ WHERE u.id != ? AND u.id != ?${filter.sql}
2190
+ ORDER BY u.email
2191
+ LIMIT ?`,
2192
+ [callerUserId, ANON_USER_ID, ...filter.args, SUGGEST_FETCH]
2193
+ );
2194
+ }
2195
+ const args = [callerUserId, ANON_USER_ID];
2196
+ const myNests = () => {
2197
+ args.push(callerUserId, callerUserId);
2198
+ return `SELECT id FROM nests WHERE user_id = ?
2199
+ UNION SELECT nest_id FROM nest_collaborators WHERE user_id = ?`;
2200
+ };
2201
+ const ownedNests = myNests();
2202
+ const collabNests = myNests();
2203
+ const stewardNests = myNests();
2204
+ const sql = `SELECT u.id AS user_id, u.email AS email, u.name AS name, 'nest' AS source
2205
+ FROM users u
2206
+ WHERE u.id != ? AND u.id != ?
2207
+ AND (u.id IN (SELECT user_id FROM nests WHERE id IN (${ownedNests}))
2208
+ OR u.id IN (SELECT user_id FROM nest_collaborators
2209
+ WHERE nest_id IN (${collabNests}))
2210
+ OR LOWER(u.email) IN (SELECT LOWER(user_email) FROM stewards
2211
+ WHERE is_active = 1
2212
+ AND nest_id IN (${stewardNests})))${filter.sql}
2213
+ ORDER BY u.email
2214
+ LIMIT ?`;
2215
+ args.push(...filter.args, SUGGEST_FETCH);
2216
+ return await db.all(sql, args);
2217
+ })();
2218
+ const byEmail = /* @__PURE__ */ new Map();
2219
+ const add = (email, name, source) => {
2220
+ const addr = (email || "").trim();
2221
+ const key = addr.toLowerCase();
2222
+ if (!addr || !key || key === callerEmail || byEmail.has(key)) return;
2223
+ const person = { email: addr, name: name ?? null, source };
2224
+ if (!matchesQuery(person, q)) return;
2225
+ byEmail.set(key, person);
2226
+ };
2227
+ for (const r of rows) add(r.email, r.name, r.source);
2228
+ const teamEmails = /* @__PURE__ */ new Set();
2229
+ for (const team of wholeDirectory ? [] : await listTeamsForUser(callerUserId)) {
2230
+ for (const member of team.members ?? []) {
2231
+ const key = (member.email || "").trim().toLowerCase();
2232
+ if (key && key !== callerEmail && !byEmail.has(key)) teamEmails.add(key);
2233
+ }
2234
+ }
2235
+ if (teamEmails.size > 0) {
2236
+ const list = [...teamEmails].slice(0, SUGGEST_FETCH);
2237
+ const named = await db.all(
2238
+ `SELECT email, name FROM users
2239
+ WHERE LOWER(email) IN (${list.map(() => "?").join(",")})`,
2240
+ list
2241
+ );
2242
+ const nameOf = new Map(
2243
+ named.map((u) => [u.email.toLowerCase(), u.name ?? null])
2244
+ );
2245
+ for (const email of list) add(email, nameOf.get(email) ?? null, "team");
2246
+ }
2247
+ return [...byEmail.values()].sort(
2248
+ (a, b) => (a.name || a.email).localeCompare(b.name || b.email, void 0, {
2249
+ sensitivity: "base"
2250
+ })
2251
+ ).slice(0, cap);
2040
2252
  }
2041
2253
 
2042
2254
  // src/governance/mention-notify.ts
@@ -2047,48 +2259,172 @@ async function canOpenNode(nestId, nodeId, userId, email) {
2047
2259
  }
2048
2260
  return canReadNode(nestId, nodeId, userId, email);
2049
2261
  }
2262
+ async function resolveRecipients(params) {
2263
+ const { nestId, nodeId, names, actorEmail } = params;
2264
+ if (names.length === 0) return [];
2265
+ const roster = await listNestPeople(nestId);
2266
+ const actor = actorEmail.toLowerCase();
2267
+ const candidates = /* @__PURE__ */ new Map();
2268
+ const outsiders = [];
2269
+ for (const name of names) {
2270
+ const member = resolveMention(roster, name);
2271
+ const person = member ?? await findDirectoryPerson(name);
2272
+ if (!person?.userId) continue;
2273
+ const key = person.email.toLowerCase();
2274
+ if (key === actor || candidates.has(key)) continue;
2275
+ const r = { email: person.email, userId: person.userId };
2276
+ candidates.set(key, r);
2277
+ if (!member) outsiders.push(r);
2278
+ if (candidates.size >= MAX_MENTION_RECIPIENTS) break;
2279
+ }
2280
+ if (outsiders.length) await inviteMentioned(nestId, actorEmail, outsiders);
2281
+ const checked = await Promise.all(
2282
+ [...candidates.values()].map(async (r) => ({
2283
+ r,
2284
+ allowed: await canOpenNode(nestId, nodeId, r.userId, r.email)
2285
+ }))
2286
+ );
2287
+ return checked.filter((c) => c.allowed).map((c) => c.r);
2288
+ }
2289
+ async function inviteMentioned(nestId, actorEmail, outsiders) {
2290
+ const db = getDb();
2291
+ const actor = await db.get("SELECT id FROM users WHERE LOWER(email) = LOWER(?)", [
2292
+ actorEmail
2293
+ ]);
2294
+ if (!actor) return;
2295
+ const access = await resolveNestAccess(nestId, actor.id);
2296
+ if (permissionLevel(access.permission) < permissionLevel("write")) return;
2297
+ for (const r of outsiders) {
2298
+ await db.run(
2299
+ `INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by)
2300
+ SELECT ?, ?, ?, 'read', ?
2301
+ WHERE NOT EXISTS (SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?)`,
2302
+ [uuid5(), nestId, r.userId, actor.id, nestId, r.userId]
2303
+ );
2304
+ }
2305
+ }
2306
+ async function insertInboxRows(params) {
2307
+ const db = getDb();
2308
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2309
+ let created = 0;
2310
+ for (const r of params.recipients) {
2311
+ await db.run(
2312
+ `INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
2313
+ VALUES (?, ?, ?, 'mention', ?, ?, ?)`,
2314
+ [uuid5(), params.nestId, r.email.toLowerCase(), params.nodeId, params.message, now]
2315
+ );
2316
+ created++;
2317
+ }
2318
+ return created;
2319
+ }
2320
+ async function deliverMentionMessages(params) {
2321
+ const { nestId, nodeId, recipients, channelMessage, actorEmail, excerpt: excerpt2, ctx } = params;
2322
+ let throttled = 0;
2323
+ const channelReachable = await hasAnyConnector(nestId, "mention");
2324
+ if (channelReachable && canDeliver("channel", nestId, actorEmail)) {
2325
+ void dispatchEvent({
2326
+ kind: "mention",
2327
+ nestId,
2328
+ subjectId: nodeId,
2329
+ actor: actorEmail,
2330
+ message: channelMessage
2331
+ });
2332
+ } else if (channelReachable) {
2333
+ throttled++;
2334
+ }
2335
+ for (const r of isEmailConfigured() ? recipients : []) {
2336
+ if (!canDeliver("email", nestId, actorEmail)) {
2337
+ throttled++;
2338
+ continue;
2339
+ }
2340
+ void sendEmailToRecipient(r.email, {
2341
+ status: "mention",
2342
+ docTitle: ctx.docTitle,
2343
+ path: ctx.path,
2344
+ link: ctx.link,
2345
+ actor: actorEmail,
2346
+ note: excerpt2 || null
2347
+ });
2348
+ }
2349
+ if (throttled > 0) {
2350
+ console.warn(
2351
+ `[notify] mention delivery budget exhausted for ${actorEmail} in ${nestId} \u2014 ${throttled} outbound message(s) skipped; inbox rows unaffected`
2352
+ );
2353
+ }
2354
+ }
2355
+ var MENTION_DELIVERY_BUDGET = { max: 50, windowMs: 5 * 6e4 };
2356
+ var MENTION_CHANNEL_BUDGET = { max: 25, windowMs: 5 * 6e4 };
2357
+ function canDeliver(kind, nestId, actorEmail) {
2358
+ return tryConsume(
2359
+ `mention-${kind}:${nestId}:${actorEmail.toLowerCase()}`,
2360
+ kind === "email" ? MENTION_DELIVERY_BUDGET : MENTION_CHANNEL_BUDGET
2361
+ );
2362
+ }
2363
+ function channelSentence(actorEmail, recipients, where) {
2364
+ const names = recipients.map((r) => r.email).join(", ");
2365
+ return `${actorEmail} mentioned ${names} ${where}`;
2366
+ }
2367
+ function excerptOf(body, max = 140) {
2368
+ const s = (body || "").replace(/\s+/g, " ").trim();
2369
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
2370
+ }
2050
2371
  async function notifyMentions(params) {
2051
- const { nestId, nodeId, previousBody, nextBody, actorEmail } = params;
2372
+ const { nestId, nodeId, previousBody, nextBody, actorEmail, baseUrl } = params;
2052
2373
  try {
2053
2374
  const names = newMentions(previousBody, nextBody);
2054
- if (names.length === 0) return 0;
2055
- const people = await listNestPeople(nestId);
2056
- const actor = actorEmail.toLowerCase();
2057
- const recipients = /* @__PURE__ */ new Map();
2058
- for (const name of names) {
2059
- const person = resolveMention(people, name);
2060
- if (!person?.userId) continue;
2061
- const key = person.email.toLowerCase();
2062
- if (key === actor || recipients.has(key)) continue;
2063
- recipients.set(key, { email: person.email, userId: person.userId });
2064
- }
2065
- if (recipients.size === 0) return 0;
2066
- const label = await describeNode(nestId, nodeId);
2067
- const now = (/* @__PURE__ */ new Date()).toISOString();
2068
- const db = getDb();
2069
- let created = 0;
2070
- for (const { email, userId } of recipients.values()) {
2071
- if (!await canOpenNode(nestId, nodeId, userId, email)) continue;
2072
- await db.run(
2073
- `INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
2074
- VALUES (?, ?, ?, 'mention', ?, ?, ?)`,
2075
- [
2076
- uuid4(),
2077
- nestId,
2078
- email.toLowerCase(),
2079
- nodeId,
2080
- `${actorEmail} mentioned you in "${label}"`,
2081
- now
2082
- ]
2083
- );
2084
- created++;
2085
- }
2375
+ const recipients = await resolveRecipients({ nestId, nodeId, names, actorEmail });
2376
+ if (recipients.length === 0) return 0;
2377
+ const ctx = await buildDocContext(nestId, nodeId, baseUrl);
2378
+ const message = `${actorEmail} mentioned you in "${ctx.label}"`;
2379
+ const created = await insertInboxRows({ nestId, nodeId, recipients, message });
2380
+ await deliverMentionMessages({
2381
+ nestId,
2382
+ nodeId,
2383
+ recipients,
2384
+ channelMessage: channelSentence(
2385
+ actorEmail,
2386
+ recipients,
2387
+ `in *${ctx.label}*`
2388
+ ),
2389
+ actorEmail,
2390
+ excerpt: "",
2391
+ ctx
2392
+ });
2086
2393
  return created;
2087
2394
  } catch (err) {
2088
2395
  console.error("[notify] mention fan-out failed", nestId, nodeId, err);
2089
2396
  return 0;
2090
2397
  }
2091
2398
  }
2399
+ async function notifyCommentMentions(params) {
2400
+ const { nestId, nodeId, body, actorEmail, baseUrl } = params;
2401
+ try {
2402
+ const names = extractMentions(body || "");
2403
+ const recipients = await resolveRecipients({ nestId, nodeId, names, actorEmail });
2404
+ if (recipients.length === 0) return 0;
2405
+ const ctx = await buildDocContext(nestId, nodeId, baseUrl);
2406
+ const excerpt2 = excerptOf(body);
2407
+ const message = `${actorEmail} mentioned you in a comment on "${ctx.label}"`;
2408
+ const created = await insertInboxRows({ nestId, nodeId, recipients, message });
2409
+ await deliverMentionMessages({
2410
+ nestId,
2411
+ nodeId,
2412
+ recipients,
2413
+ channelMessage: channelSentence(
2414
+ actorEmail,
2415
+ recipients,
2416
+ `in a comment on *${ctx.label}*`
2417
+ ),
2418
+ actorEmail,
2419
+ excerpt: excerpt2,
2420
+ ctx
2421
+ });
2422
+ return created;
2423
+ } catch (err) {
2424
+ console.error("[notify] comment mention fan-out failed", nestId, nodeId, err);
2425
+ return 0;
2426
+ }
2427
+ }
2092
2428
 
2093
2429
  // src/nodes/service.ts
2094
2430
  async function userIdFromEmail(email) {
@@ -2312,8 +2648,9 @@ function engineScopesFolders() {
2312
2648
  }
2313
2649
  return folderScopeSupported;
2314
2650
  }
2651
+ var DERIVED_ANNOTATIONS_SUFFIX = "--annotations";
2315
2652
  function isDerivedAnnotationsNode(id) {
2316
- return id.endsWith("--annotations");
2653
+ return id.endsWith(DERIVED_ANNOTATIONS_SUFFIX);
2317
2654
  }
2318
2655
  async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
2319
2656
  if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
@@ -2647,11 +2984,11 @@ async function autoPublishReason(nestId, userEmail, doc) {
2647
2984
  }
2648
2985
  return await actorAutoPublishes(nestId, userEmail) ? "owner/admin self-approve" : null;
2649
2986
  }
2650
- async function createNode(nestId, input, userEmail) {
2987
+ async function createNode(nestId, input, userEmail, opts = {}) {
2651
2988
  const generatedId = !input.id && !slugify(input.title);
2652
2989
  for (let attempt = 0; ; attempt++) {
2653
2990
  try {
2654
- return await createNodeOnce(nestId, input, userEmail);
2991
+ return await createNodeOnce(nestId, input, userEmail, opts);
2655
2992
  } catch (err) {
2656
2993
  if (!generatedId || attempt >= 3 || !(err instanceof ConflictError)) {
2657
2994
  throw err;
@@ -2659,7 +2996,7 @@ async function createNode(nestId, input, userEmail) {
2659
2996
  }
2660
2997
  }
2661
2998
  }
2662
- async function createNodeOnce(nestId, input, userEmail) {
2999
+ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
2663
3000
  const { storage } = await engineCache.get(nestId);
2664
3001
  assertTitleLength(input.title);
2665
3002
  assertTitleReadable(input.title);
@@ -2792,13 +3129,15 @@ async function createNodeOnce(nestId, input, userEmail) {
2792
3129
  });
2793
3130
  }
2794
3131
  await trackEvent("node.create", { nestId, nodeId: id });
2795
- await notifyMentions({
2796
- nestId,
2797
- nodeId: id,
2798
- previousBody: "",
2799
- nextBody: node.body || "",
2800
- actorEmail: userEmail
2801
- });
3132
+ if (opts.notifyMentions !== false) {
3133
+ await notifyMentions({
3134
+ nestId,
3135
+ nodeId: id,
3136
+ previousBody: "",
3137
+ nextBody: node.body || "",
3138
+ actorEmail: userEmail
3139
+ });
3140
+ }
2802
3141
  return { node, version: savedVersion };
2803
3142
  });
2804
3143
  }
@@ -2892,7 +3231,7 @@ async function registerImportedDocuments(nestId, userEmail) {
2892
3231
  await trackEvent("nest.import.documents", { nestId, registered: docs.length });
2893
3232
  return docs.length;
2894
3233
  }
2895
- async function updateNode(nestId, nodeId, patch, userEmail) {
3234
+ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
2896
3235
  return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
2897
3236
  try {
2898
3237
  const { storage, versions: versionManager } = await engineCache.get(
@@ -3044,13 +3383,15 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
3044
3383
  );
3045
3384
  responseVersion = publishedVersion;
3046
3385
  }
3047
- await notifyMentions({
3048
- nestId,
3049
- nodeId,
3050
- previousBody,
3051
- nextBody: node.body || "",
3052
- actorEmail: userEmail
3053
- });
3386
+ if (opts.notifyMentions !== false) {
3387
+ await notifyMentions({
3388
+ nestId,
3389
+ nodeId,
3390
+ previousBody,
3391
+ nextBody: node.body || "",
3392
+ actorEmail: userEmail
3393
+ });
3394
+ }
3054
3395
  return { node, version: responseVersion };
3055
3396
  } catch (err) {
3056
3397
  throw toSafeError(
@@ -3632,8 +3973,13 @@ nestRoutes.get("/", async (c) => {
3632
3973
  }
3633
3974
  const out = await Promise.all(deduped.map(annotate));
3634
3975
  const docRows = await db.all(
3635
- `SELECT nest_id, COUNT(DISTINCT node_id) AS c, MAX(created_at) AS last_activity
3636
- FROM node_versions GROUP BY nest_id`
3976
+ `SELECT nest_id,
3977
+ COUNT(DISTINCT CASE
3978
+ WHEN ${suffixOf(db, "node_id", DERIVED_ANNOTATIONS_SUFFIX.length)} <> ?
3979
+ THEN node_id END) AS c,
3980
+ MAX(created_at) AS last_activity
3981
+ FROM node_versions GROUP BY nest_id`,
3982
+ [DERIVED_ANNOTATIONS_SUFFIX]
3637
3983
  );
3638
3984
  const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
3639
3985
  const activityByNest = new Map(docRows.map((r) => [r.nest_id, r.last_activity]));
@@ -3675,6 +4021,7 @@ nestRoutes.get("/", async (c) => {
3675
4021
  );
3676
4022
  for (const r of rows) {
3677
4023
  const targets = grantTargetsByNest.get(r.nest_id) ?? [];
4024
+ if (isDerivedAnnotationsNode(r.node_id)) continue;
3678
4025
  if (grantCoversNode(targets, r.node_id)) {
3679
4026
  grantedDocsByNest.set(r.nest_id, (grantedDocsByNest.get(r.nest_id) ?? 0) + 1);
3680
4027
  }
@@ -3960,7 +4307,7 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
3960
4307
  import { Hono as Hono4 } from "hono";
3961
4308
 
3962
4309
  // src/nests/sharing-service.ts
3963
- import { v4 as uuid5 } from "uuid";
4310
+ import { v4 as uuid6 } from "uuid";
3964
4311
  var VALID_PERMISSIONS = ["read", "write", "admin"];
3965
4312
  async function addCollaborator(params) {
3966
4313
  const { nestId } = params;
@@ -3987,10 +4334,10 @@ async function addCollaborator(params) {
3987
4334
  userId = existing.id;
3988
4335
  } else {
3989
4336
  const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
3990
- userId = uuid5();
4337
+ userId = uuid6();
3991
4338
  await db.run(
3992
4339
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
3993
- [userId, email, null, await hashPassword2(uuid5())]
4340
+ [userId, email, null, await hashPassword2(uuid6())]
3994
4341
  );
3995
4342
  }
3996
4343
  }
@@ -4028,7 +4375,7 @@ async function addCollaborator(params) {
4028
4375
  [params.grantedByEmail]
4029
4376
  ))?.id : void 0;
4030
4377
  const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
4031
- const collabId = uuid5();
4378
+ const collabId = uuid6();
4032
4379
  await db.run(
4033
4380
  "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
4034
4381
  [collabId, nestId, userId, params.permission, granterId]
@@ -4101,7 +4448,7 @@ sharingRoutes.get("/collaborators", async (c) => {
4101
4448
  }
4102
4449
  return c.json({
4103
4450
  collaborators: enriched,
4104
- people: await listNestPeople(nestId)
4451
+ people: await withDirectory(await listNestPeople(nestId))
4105
4452
  });
4106
4453
  });
4107
4454
  sharingRoutes.post("/collaborators", async (c) => {
@@ -4145,6 +4492,22 @@ sharingRoutes.delete("/collaborators/:collabId", async (c) => {
4145
4492
  }
4146
4493
  return c.json({ removed: true });
4147
4494
  });
4495
+ sharingRoutes.get("/mentionable", async (c) => {
4496
+ const nestId = c.req.param("nestId");
4497
+ const userId = c.get("userId");
4498
+ const people = await listNestPeople(nestId);
4499
+ const callerEmail = (await resolveCallerEmail(userId)).toLowerCase();
4500
+ const onRoster = people.some((p) => p.email.toLowerCase() === callerEmail);
4501
+ if (!onRoster) {
4502
+ const access = await resolveNestAccess(nestId, userId);
4503
+ if (permissionLevel(access.permission) < permissionLevel("write")) {
4504
+ throw new ForbiddenError("Only people on this nest can see its roster");
4505
+ }
4506
+ }
4507
+ return c.json({
4508
+ people: (await withDirectory(people)).map((p) => ({ email: p.email, name: p.name }))
4509
+ });
4510
+ });
4148
4511
  sharingRoutes.patch("/visibility", async (c) => {
4149
4512
  const body = await c.req.json();
4150
4513
  if (!body.visibility || !["private", "org", "public"].includes(body.visibility)) {
@@ -4176,10 +4539,14 @@ sharingRoutes.patch("/visibility", async (c) => {
4176
4539
  });
4177
4540
  sharingRoutes.patch("/reader-mode", async (c) => {
4178
4541
  const userId = c.get("userId");
4542
+ const nestId = c.req.param("nestId");
4179
4543
  if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
4180
- throw new ForbiddenError(
4181
- "Only the server admin can configure reader mode"
4182
- );
4544
+ const access = await resolveNestAccess(nestId, userId);
4545
+ if (permissionLevel(access.permission) < permissionLevel("admin")) {
4546
+ throw new ForbiddenError(
4547
+ "Only the nest owner, a nest admin, or the server admin can configure reader mode"
4548
+ );
4549
+ }
4183
4550
  }
4184
4551
  const body = await c.req.json();
4185
4552
  if (body.enabled !== void 0 && typeof body.enabled !== "boolean") {
@@ -4202,7 +4569,6 @@ sharingRoutes.patch("/reader-mode", async (c) => {
4202
4569
  );
4203
4570
  }
4204
4571
  const db = getDb();
4205
- const nestId = c.req.param("nestId");
4206
4572
  const exists = await db.get("SELECT id FROM nests WHERE id = ?", [
4207
4573
  nestId
4208
4574
  ]);
@@ -4222,7 +4588,7 @@ sharingRoutes.patch("/reader-mode", async (c) => {
4222
4588
 
4223
4589
  // src/nests/grant-routes.ts
4224
4590
  import { Hono as Hono5 } from "hono";
4225
- import { v4 as uuid6 } from "uuid";
4591
+ import { v4 as uuid7 } from "uuid";
4226
4592
  var grantRoutes = new Hono5();
4227
4593
  async function requireManage(c, nestId) {
4228
4594
  if (!await canManageStewards(nestId, c.get("userId"))) {
@@ -4237,10 +4603,10 @@ async function resolveGrantee(email) {
4237
4603
  const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
4238
4604
  if (existing) return existing.id;
4239
4605
  const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
4240
- const id = uuid6();
4606
+ const id = uuid7();
4241
4607
  await db.run(
4242
4608
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
4243
- [id, e, null, await hashPassword2(uuid6())]
4609
+ [id, e, null, await hashPassword2(uuid7())]
4244
4610
  );
4245
4611
  return id;
4246
4612
  }
@@ -4342,7 +4708,7 @@ grantRoutes.delete("/:id", async (c) => {
4342
4708
  import { Hono as Hono6 } from "hono";
4343
4709
 
4344
4710
  // src/nests/publish-service.ts
4345
- import { v4 as uuid7 } from "uuid";
4711
+ import { v4 as uuid8 } from "uuid";
4346
4712
  import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
4347
4713
  var MODES = ["public", "email", "code", "invite"];
4348
4714
  var LINK_TTL_SECONDS = 60 * 60;
@@ -4411,7 +4777,7 @@ async function createPublish(params) {
4411
4777
  );
4412
4778
  }
4413
4779
  const db = getDb();
4414
- const id = uuid7();
4780
+ const id = uuid8();
4415
4781
  const slug = newSlug();
4416
4782
  const created = nowIso();
4417
4783
  const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
@@ -4442,7 +4808,7 @@ async function createPublish(params) {
4442
4808
  await db.run(
4443
4809
  `INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
4444
4810
  VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
4445
- [uuid7(), id, email, created]
4811
+ [uuid8(), id, email, created]
4446
4812
  );
4447
4813
  }
4448
4814
  }
@@ -4497,7 +4863,7 @@ async function mintMagicLink(publishId, email) {
4497
4863
  await getDb().run(
4498
4864
  `INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
4499
4865
  VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
4500
- [uuid7(), publishId, e, sha256Hex(token), expires, nowIso()]
4866
+ [uuid8(), publishId, e, sha256Hex(token), expires, nowIso()]
4501
4867
  );
4502
4868
  return token;
4503
4869
  }
@@ -4527,7 +4893,7 @@ async function createSession2(publishId, email) {
4527
4893
  await getDb().run(
4528
4894
  `INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
4529
4895
  VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
4530
- [uuid7(), publishId, email, sha256Hex(token), expires, nowIso()]
4896
+ [uuid8(), publishId, email, sha256Hex(token), expires, nowIso()]
4531
4897
  );
4532
4898
  return token;
4533
4899
  }
@@ -4544,7 +4910,7 @@ async function logView(publishId, email) {
4544
4910
  await getDb().run(
4545
4911
  `INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
4546
4912
  VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
4547
- [uuid7(), publishId, email, nowIso()]
4913
+ [uuid8(), publishId, email, nowIso()]
4548
4914
  );
4549
4915
  }
4550
4916
  async function listAllowlist(publishId) {
@@ -4637,7 +5003,7 @@ import { Hono as Hono7 } from "hono";
4637
5003
  import MarkdownIt from "markdown-it";
4638
5004
 
4639
5005
  // src/annotations/service.ts
4640
- import { v4 as uuid8 } from "uuid";
5006
+ import { v4 as uuid9 } from "uuid";
4641
5007
 
4642
5008
  // src/annotations/projection.ts
4643
5009
  var MAX_CONTEXT_CHARS = 250;
@@ -4762,6 +5128,10 @@ async function rowToThread(row) {
4762
5128
  async function getThreadRow(threadId) {
4763
5129
  return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
4764
5130
  }
5131
+ async function getThread(threadId) {
5132
+ const row = await getThreadRow(threadId);
5133
+ return row ? await rowToThread(row) : null;
5134
+ }
4765
5135
  async function countThreadsByNode(nestId) {
4766
5136
  const rows = await getDb().all(
4767
5137
  `SELECT node_id, CAST(COUNT(*) AS INTEGER) AS count, MAX(created_at) AS last_at
@@ -4787,14 +5157,14 @@ async function createThread(nestId, nodeId, input, authorEmail) {
4787
5157
  throw new Error("comment body is required");
4788
5158
  }
4789
5159
  const db = getDb();
4790
- const id = uuid8();
5160
+ const id = uuid9();
4791
5161
  const anchor = clampAnchor(input.anchor);
4792
5162
  const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
4793
5163
  await db.transaction(async (tx) => {
4794
5164
  await tx.run(
4795
5165
  `INSERT INTO annotation_threads
4796
- (id, nest_id, node_id, snapshot_version, anchor_json, status, created_by)
4797
- VALUES (?, ?, ?, ?, ?, 'open', ?)`,
5166
+ (id, nest_id, node_id, snapshot_version, anchor_json, status, created_by, created_at)
5167
+ VALUES (?, ?, ?, ?, ?, 'open', ?, ${nowMsExpr(db)})`,
4798
5168
  [
4799
5169
  id,
4800
5170
  nestId,
@@ -4805,8 +5175,9 @@ async function createThread(nestId, nodeId, input, authorEmail) {
4805
5175
  ]
4806
5176
  );
4807
5177
  await tx.run(
4808
- "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
4809
- [uuid8(), id, authorEmail, body]
5178
+ `INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
5179
+ VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
5180
+ [uuid9(), id, authorEmail, body]
4810
5181
  );
4811
5182
  });
4812
5183
  return rowToThread(await getThreadRow(id));
@@ -4824,9 +5195,11 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
4824
5195
  throw new Error("comment body is required");
4825
5196
  }
4826
5197
  await getScopedThreadRow(threadId, nestId, nodeId);
4827
- await getDb().run(
4828
- "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
4829
- [uuid8(), threadId, authorEmail, trimmed]
5198
+ const db = getDb();
5199
+ await db.run(
5200
+ `INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
5201
+ VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
5202
+ [uuid9(), threadId, authorEmail, trimmed]
4830
5203
  );
4831
5204
  return rowToThread(await getThreadRow(threadId));
4832
5205
  }
@@ -4913,7 +5286,13 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
4913
5286
  const markdown2 = projectThreadsToMarkdown(title, threads);
4914
5287
  const derivedId = derivedAnnotationsId(nodeId);
4915
5288
  try {
4916
- await updateNode(nestId, derivedId, { content: markdown2 }, userEmail);
5289
+ await updateNode(
5290
+ nestId,
5291
+ derivedId,
5292
+ { content: markdown2 },
5293
+ userEmail,
5294
+ { notifyMentions: false }
5295
+ );
4917
5296
  } catch (err) {
4918
5297
  if (err instanceof NotFoundError) {
4919
5298
  await createNode(
@@ -4925,7 +5304,8 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
4925
5304
  type: "document",
4926
5305
  tags: ["annotations"]
4927
5306
  },
4928
- userEmail
5307
+ userEmail,
5308
+ { notifyMentions: false }
4929
5309
  );
4930
5310
  } else {
4931
5311
  throw err;
@@ -5350,8 +5730,19 @@ nestTeamRoutes.delete("/:teamId", async (c) => {
5350
5730
  return c.json({ teams });
5351
5731
  });
5352
5732
 
5353
- // src/governance/team-routes.ts
5733
+ // src/nests/people-routes.ts
5354
5734
  import { Hono as Hono9 } from "hono";
5735
+ var peopleRoutes = new Hono9();
5736
+ peopleRoutes.get("/suggest", async (c) => {
5737
+ const userId = c.get("userId");
5738
+ const q = c.req.query("q") ?? "";
5739
+ const rawLimit = Number(c.req.query("limit"));
5740
+ const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : void 0;
5741
+ return c.json({ people: await suggestPeopleForUser(userId, q, limit) });
5742
+ });
5743
+
5744
+ // src/governance/team-routes.ts
5745
+ import { Hono as Hono10 } from "hono";
5355
5746
 
5356
5747
  // src/governance/promptowl-teams.ts
5357
5748
  var PROMPTOWL_SOURCE = "promptowl";
@@ -5498,7 +5889,7 @@ async function desiredRoster(poTeam, callerEmail) {
5498
5889
  }
5499
5890
 
5500
5891
  // src/governance/team-routes.ts
5501
- var teamRoutes = new Hono9();
5892
+ var teamRoutes = new Hono10();
5502
5893
  var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
5503
5894
  var CONNECT_REQUIRED = "promptowl_connect_required";
5504
5895
  var CONNECT_REQUIRED_STATUS = 428;
@@ -5683,7 +6074,7 @@ function toSearchHit(node, query) {
5683
6074
  }
5684
6075
 
5685
6076
  // src/nodes/routes.ts
5686
- import { Hono as Hono10 } from "hono";
6077
+ import { Hono as Hono11 } from "hono";
5687
6078
 
5688
6079
  // src/nodes/markdown-export.ts
5689
6080
  import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
@@ -5735,7 +6126,7 @@ ${m[1]}
5735
6126
  }
5736
6127
 
5737
6128
  // src/nodes/routes.ts
5738
- var nodeRoutes = new Hono10();
6129
+ var nodeRoutes = new Hono11();
5739
6130
  function nodeAsMarkdown(response, nodeId) {
5740
6131
  return nodeToMarkdown({
5741
6132
  id: nodeId,
@@ -5817,7 +6208,7 @@ nodeRoutes.post("/", async (c) => {
5817
6208
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
5818
6209
  const nestId = c.req.param("nestId");
5819
6210
  const nodeId = c.req.param("nodeId");
5820
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-5HBQGDMK.js");
6211
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-32UHIRQ5.js");
5821
6212
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
5822
6213
  nestId,
5823
6214
  nodeId
@@ -5838,7 +6229,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
5838
6229
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
5839
6230
  const nestId = c.req.param("nestId");
5840
6231
  const nodeId = c.req.param("nodeId");
5841
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-XFWTENTN.js");
6232
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-A7PNLWG5.js");
5842
6233
  const history = await getReviewHistory2(nestId, nodeId);
5843
6234
  return c.json({ reviews: history });
5844
6235
  });
@@ -6082,13 +6473,13 @@ async function getUserEmail(c) {
6082
6473
  }
6083
6474
 
6084
6475
  // src/annotations/routes.ts
6085
- import { Hono as Hono11 } from "hono";
6476
+ import { Hono as Hono12 } from "hono";
6086
6477
 
6087
6478
  // src/annotations/types.ts
6088
6479
  var ARTIFACT_NODE_TYPE = "artifact";
6089
6480
 
6090
6481
  // src/annotations/routes.ts
6091
- var annotationRoutes = new Hono11();
6482
+ var annotationRoutes = new Hono12();
6092
6483
  function getNodeId(c) {
6093
6484
  const raw = c.req.param("nodeId");
6094
6485
  try {
@@ -6129,6 +6520,13 @@ annotationRoutes.post("/:nodeId{.+}/annotations", async (c) => {
6129
6520
  },
6130
6521
  userEmail
6131
6522
  );
6523
+ await notifyCommentMentions({
6524
+ nestId,
6525
+ nodeId,
6526
+ body: input.body,
6527
+ actorEmail: userEmail,
6528
+ baseUrl: requestBaseUrl(c.req.url)
6529
+ });
6132
6530
  await syncAnnotationsNode(nestId, nodeId, userEmail);
6133
6531
  return c.json({ thread }, 201);
6134
6532
  });
@@ -6144,6 +6542,13 @@ async function handleAnnotationReply(c, nodeId, threadId) {
6144
6542
  throw new ValidationError("body is required");
6145
6543
  }
6146
6544
  const thread = await addComment(nestId, nodeId, threadId, userEmail, input.body);
6545
+ await notifyCommentMentions({
6546
+ nestId,
6547
+ nodeId,
6548
+ body: input.body,
6549
+ actorEmail: userEmail,
6550
+ baseUrl: requestBaseUrl(c.req.url)
6551
+ });
6147
6552
  await syncAnnotationsNode(nestId, nodeId, userEmail);
6148
6553
  return c.json({ thread });
6149
6554
  }
@@ -6202,7 +6607,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
6202
6607
  });
6203
6608
 
6204
6609
  // src/nodes/query-routes.ts
6205
- import { Hono as Hono12 } from "hono";
6610
+ import { Hono as Hono13 } from "hono";
6206
6611
 
6207
6612
  // src/tables/service.ts
6208
6613
  function splitCsvLine(line) {
@@ -6836,7 +7241,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
6836
7241
  }
6837
7242
 
6838
7243
  // src/nodes/query-routes.ts
6839
- var queryRoutes = new Hono12();
7244
+ var queryRoutes = new Hono13();
6840
7245
  queryRoutes.get("/changes", async (c) => {
6841
7246
  const nestId = c.req.param("nestId");
6842
7247
  const userId = c.get("userId");
@@ -7398,7 +7803,7 @@ queryRoutes.post("/publish", async (c) => {
7398
7803
  });
7399
7804
 
7400
7805
  // src/mcp/routes.ts
7401
- import { Hono as Hono14 } from "hono";
7806
+ import { Hono as Hono15 } from "hono";
7402
7807
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7403
7808
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
7404
7809
 
@@ -7478,10 +7883,10 @@ async function listTraceEvents(filters = {}) {
7478
7883
  }
7479
7884
 
7480
7885
  // src/mcp/workflow-tools.ts
7481
- import { v4 as uuid11 } from "uuid";
7886
+ import { v4 as uuid12 } from "uuid";
7482
7887
 
7483
7888
  // src/workflow/run-service.ts
7484
- import { v4 as uuid9 } from "uuid";
7889
+ import { v4 as uuid10 } from "uuid";
7485
7890
  var RUNNABLE_TYPES = ["agent", "skill"];
7486
7891
  async function flowSubgraph(nestId, agentNode) {
7487
7892
  const db = getDb();
@@ -7611,7 +8016,7 @@ async function triggerRun(opts) {
7611
8016
  }
7612
8017
  const edges = await flowSubgraph(nestId, agentNode);
7613
8018
  const definitions = await referencedDefinitions(nestId, edges);
7614
- const id = `run_${uuid9()}`;
8019
+ const id = `run_${uuid10()}`;
7615
8020
  const db = getDb();
7616
8021
  await db.transaction(async (tx) => {
7617
8022
  if (parentRunId) {
@@ -7797,8 +8202,8 @@ async function listRuns(nestId, opts) {
7797
8202
  }
7798
8203
 
7799
8204
  // src/workflow/edge-routes.ts
7800
- import { Hono as Hono13 } from "hono";
7801
- import { v4 as uuid10 } from "uuid";
8205
+ import { Hono as Hono14 } from "hono";
8206
+ import { v4 as uuid11 } from "uuid";
7802
8207
  function toResponse(e, t) {
7803
8208
  return {
7804
8209
  id: e.id,
@@ -7859,7 +8264,9 @@ async function validateCondition(nestId, mode, condition) {
7859
8264
  async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
7860
8265
  if (fromNode === toNode) {
7861
8266
  throw new ConflictError(
7862
- `This step loops back on itself: ${await nodeTrail(db, nestId, [fromNode])}. A flow can't return to a step it already ran.`
8267
+ `This step loops back on itself: ${await nodeTrail(db, nestId, [fromNode])}.
8268
+
8269
+ A flow can't return to a step it already ran.`
7863
8270
  );
7864
8271
  }
7865
8272
  const rows = await db.all(
@@ -7888,7 +8295,9 @@ async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
7888
8295
  }
7889
8296
  path.reverse().push(toNode);
7890
8297
  throw new ConflictError(
7891
- `These steps loop back on each other: ${await nodeTrail(db, nestId, path)}. A flow can't return to a step it already ran.`
8298
+ `These steps loop back on each other: ${await nodeTrail(db, nestId, path)}.
8299
+
8300
+ A flow can't return to a step it already ran.`
7892
8301
  );
7893
8302
  }
7894
8303
  for (const nb of adj.get(cur) ?? []) {
@@ -7912,7 +8321,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
7912
8321
  );
7913
8322
  }
7914
8323
  }
7915
- var edgeRoutes = new Hono13();
8324
+ var edgeRoutes = new Hono14();
7916
8325
  edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
7917
8326
  const nestId = c.req.param("nestId");
7918
8327
  const db = getDb();
@@ -7980,7 +8389,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
7980
8389
  );
7981
8390
  const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
7982
8391
  const now = (/* @__PURE__ */ new Date()).toISOString();
7983
- const id = uuid10();
8392
+ const id = uuid11();
7984
8393
  await db.transaction(async (tx) => {
7985
8394
  await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
7986
8395
  if (type.is_flow) {
@@ -8381,7 +8790,7 @@ async function createEdge(ctx, args) {
8381
8790
  );
8382
8791
  const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
8383
8792
  const now = (/* @__PURE__ */ new Date()).toISOString();
8384
- const id = uuid11();
8793
+ const id = uuid12();
8385
8794
  await db.transaction(async (tx) => {
8386
8795
  await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
8387
8796
  if (type.is_flow) {
@@ -8445,7 +8854,7 @@ async function upsertEdgeType(ctx, args) {
8445
8854
  `INSERT INTO edge_types
8446
8855
  (id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
8447
8856
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
8448
- [uuid11(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
8857
+ [uuid12(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
8449
8858
  );
8450
8859
  }
8451
8860
  });
@@ -8654,6 +9063,43 @@ var TOOL_DEFINITIONS = [
8654
9063
  }
8655
9064
  }
8656
9065
  },
9066
+ {
9067
+ name: "context_comment",
9068
+ description: "Leave a comment on a document \u2014 the same threads the web UI's comments panel shows. Omit `thread` to open a new one; pass a thread id (from context_comments) to reply to it. Use this to answer feedback you were given, or to raise something you noticed while working. Anyone who can read the document may comment; `@name` in the body notifies that person.",
9069
+ inputSchema: {
9070
+ type: "object",
9071
+ properties: {
9072
+ title: { type: "string", description: "Title of the node" },
9073
+ id: { type: "string", description: "ID of the node" },
9074
+ body: { type: "string", description: "The comment text" },
9075
+ thread: {
9076
+ type: "string",
9077
+ description: "Thread id to reply to (from context_comments). Omit to open a new thread on the document."
9078
+ }
9079
+ },
9080
+ required: ["body"]
9081
+ }
9082
+ },
9083
+ {
9084
+ name: "context_resolve_comment",
9085
+ description: "Mark a comment thread resolved once you have addressed it, or reopen one that was closed too early. Records you as the resolver, exactly as the web UI does. Resolve only threads you actually acted on.",
9086
+ inputSchema: {
9087
+ type: "object",
9088
+ properties: {
9089
+ title: { type: "string", description: "Title of the node" },
9090
+ id: { type: "string", description: "ID of the node" },
9091
+ thread: {
9092
+ type: "string",
9093
+ description: "Thread id, from context_comments"
9094
+ },
9095
+ reopen: {
9096
+ type: "boolean",
9097
+ description: "Reopen the thread instead of resolving it"
9098
+ }
9099
+ },
9100
+ required: ["thread"]
9101
+ }
9102
+ },
8657
9103
  {
8658
9104
  name: "context_create",
8659
9105
  description: "Create a new knowledge node in the vault. The title must not already be in use \u2014 these tools resolve documents by title, so a duplicate is refused rather than filed as an unreachable twin. To change an existing document use context_update; to refile it use context_move.",
@@ -9359,8 +9805,8 @@ ${parts.join(
9359
9805
  node = docs.find((n) => n.id === args.id);
9360
9806
  }
9361
9807
  if (!node) return `Node not found: ${args.title || args.id}`;
9362
- if (await resolveLlmBody(ctx, node) === null) {
9363
- return `Node "${node.frontmatter.title}" has no approved version yet \u2014 not available to AI.`;
9808
+ if (!await canReadNode(nestId, node.id, ctx.userId, userEmail)) {
9809
+ return `Node not found: ${args.title || args.id}`;
9364
9810
  }
9365
9811
  const threads = await listThreads(nestId, node.id);
9366
9812
  if (!threads.length)
@@ -9379,6 +9825,89 @@ ${quote}${comments}`;
9379
9825
 
9380
9826
  ${sections}`;
9381
9827
  }
9828
+ // ── Comments: write half of context_comments ──────────────────────────
9829
+ //
9830
+ // Both cases target the annotation store the UI's comments panel uses, so
9831
+ // an agent's comment and a human's are the same object, and both carry the
9832
+ // side effects the REST routes carry: the @mention fan-out and the derived
9833
+ // `— Annotations` projection an artifact's agents read back.
9834
+ //
9835
+ // PRIMARY permission gate, like context_create's: the /mcp middleware
9836
+ // admits read-tier callers (the transport is always POST), so the check
9837
+ // here is the real one. Commenting is READ-tier — the same rule REST
9838
+ // applies in app.ts (isAnnotationAction) — so canReadNode is the bar, not
9839
+ // write access. Deliberately NOT gated on an approved version: a reviewer
9840
+ // comments on a draft, and unlike reading a thread, writing one discloses
9841
+ // nothing (the caller supplies the text).
9842
+ case "context_comment":
9843
+ case "context_resolve_comment": {
9844
+ const body = String(args.body ?? "");
9845
+ const isResolve = toolName === "context_resolve_comment";
9846
+ if (!isResolve && !body.trim()) {
9847
+ return "body is required \u2014 the comment text.";
9848
+ }
9849
+ const threadId = args.thread ? String(args.thread) : "";
9850
+ if (isResolve && !threadId) {
9851
+ return "thread is required \u2014 the id from context_comments.";
9852
+ }
9853
+ const docs = await storage.discoverDocuments();
9854
+ let node;
9855
+ if (args.title) {
9856
+ node = docs.find(
9857
+ (n) => n.frontmatter.title.toLowerCase() === String(args.title).toLowerCase()
9858
+ );
9859
+ } else if (args.id) {
9860
+ node = docs.find((n) => n.id === String(args.id));
9861
+ }
9862
+ if (!node) {
9863
+ return toolError(
9864
+ `Node not found: ${args.title || args.id || "(no title or id given)"}`,
9865
+ "DOCUMENT_NOT_FOUND"
9866
+ );
9867
+ }
9868
+ if (!await canReadNode(nestId, node.id, ctx.userId, userEmail)) {
9869
+ return toolError(
9870
+ `Node not found: ${args.title || args.id}`,
9871
+ "DOCUMENT_NOT_FOUND"
9872
+ );
9873
+ }
9874
+ const label = node.frontmatter.title || node.id;
9875
+ if (threadId) {
9876
+ const existing = await getThread(threadId);
9877
+ if (!existing || existing.nodeId !== node.id || existing.nestId !== nestId) {
9878
+ return toolError(
9879
+ `Thread ${threadId} is not a thread on "${label}". Call context_comments to list its threads.`,
9880
+ "DOCUMENT_NOT_FOUND"
9881
+ );
9882
+ }
9883
+ }
9884
+ if (isResolve) {
9885
+ const reopen = args.reopen === true;
9886
+ const thread2 = await setThreadStatus(
9887
+ nestId,
9888
+ node.id,
9889
+ threadId,
9890
+ reopen ? "open" : "resolved",
9891
+ userEmail
9892
+ );
9893
+ await syncAnnotationsNode(nestId, node.id, userEmail);
9894
+ return reopen ? `Reopened the thread on "${label}". It is back in the open list for whoever picks it up.` : `Resolved the thread on "${label}", recorded against ${thread2.resolvedBy}.`;
9895
+ }
9896
+ const thread = threadId ? await addComment(nestId, node.id, threadId, userEmail, body) : await createThread(nestId, node.id, { body, anchor: null }, userEmail);
9897
+ await notifyCommentMentions({
9898
+ nestId,
9899
+ nodeId: node.id,
9900
+ body,
9901
+ actorEmail: userEmail,
9902
+ // Without this the notification still reaches the person, but its
9903
+ // "Open document" link falls back to PUBLIC_BASE_URL and comes out
9904
+ // empty when that isn't configured — every other notifying tool in
9905
+ // this file passes it, and the REST route passes requestBaseUrl.
9906
+ baseUrl: ctx.baseUrl
9907
+ });
9908
+ await syncAnnotationsNode(nestId, node.id, userEmail);
9909
+ return threadId ? `Replied on "${label}", in thread ${thread.id}.` : `Commented on "${label}" \u2014 opened thread ${thread.id}. Reply to it by passing that id as \`thread\`.`;
9910
+ }
9382
9911
  case "context_create": {
9383
9912
  if (!await canCreateInNest(nestId, userEmail)) {
9384
9913
  return toolError(
@@ -9927,19 +10456,19 @@ ${list}`,
9927
10456
  if (!target) return "target is required (a node id or folder prefix).";
9928
10457
  if (!["read", "write"].includes(role)) return "role must be read or write.";
9929
10458
  try {
9930
- const { createGrant: createGrant2 } = await import("./grants-service-UZ2MGDO5.js");
9931
- const db = (await import("./client-CSCHXUNX.js")).getDb();
10459
+ const { createGrant: createGrant2 } = await import("./grants-service-TBGTCO5K.js");
10460
+ const db = (await import("./client-LWDMNKX3.js")).getDb();
9932
10461
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
9933
10462
  const e = normalizeEmail2(String(args.email || ""));
9934
10463
  if (!e) return "email is required.";
9935
10464
  let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
9936
10465
  if (!row) {
9937
10466
  const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
9938
- const { v4: uuid19 } = await import("uuid");
9939
- const id = uuid19();
10467
+ const { v4: uuid20 } = await import("uuid");
10468
+ const id = uuid20();
9940
10469
  await db.run(
9941
10470
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
9942
- [id, e, null, await hashPassword2(uuid19())]
10471
+ [id, e, null, await hashPassword2(uuid20())]
9943
10472
  );
9944
10473
  row = { id };
9945
10474
  }
@@ -9994,7 +10523,7 @@ ${list}`;
9994
10523
 
9995
10524
  // src/mcp/routes.ts
9996
10525
  import { z } from "zod";
9997
- var mcpRoutes = new Hono14();
10526
+ var mcpRoutes = new Hono15();
9998
10527
  async function getUserEmail2(userId) {
9999
10528
  const db = getDb();
10000
10529
  const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
@@ -10060,8 +10589,8 @@ mcpRoutes.all("/", async (c) => {
10060
10589
  });
10061
10590
 
10062
10591
  // src/workflow/run-routes.ts
10063
- import { Hono as Hono15 } from "hono";
10064
- import { v4 as uuid12 } from "uuid";
10592
+ import { Hono as Hono16 } from "hono";
10593
+ import { v4 as uuid13 } from "uuid";
10065
10594
  var RUNNABLE_TYPES2 = ["agent", "skill"];
10066
10595
  var safeJson2 = (s) => {
10067
10596
  if (!s) return null;
@@ -10179,7 +10708,7 @@ async function buildRunBundle(nestId, agentNode) {
10179
10708
  definitions
10180
10709
  };
10181
10710
  }
10182
- var runTriggerRoutes = new Hono15();
10711
+ var runTriggerRoutes = new Hono16();
10183
10712
  async function resolveParentRun2(nestId, parentRunId, childAgent) {
10184
10713
  if (!config.FEATURE_SUBAGENT_RUNS) {
10185
10714
  throw new ValidationError(
@@ -10258,7 +10787,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
10258
10787
  }
10259
10788
  const bundle = await buildRunBundle(nestId, agentNode);
10260
10789
  const triggeredBy = await resolveCallerEmail(c.get("userId"));
10261
- const id = `run_${uuid12()}`;
10790
+ const id = `run_${uuid13()}`;
10262
10791
  await getDb().run(
10263
10792
  `INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
10264
10793
  VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
@@ -10287,7 +10816,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
10287
10816
  201
10288
10817
  );
10289
10818
  });
10290
- var runListRoutes = new Hono15();
10819
+ var runListRoutes = new Hono16();
10291
10820
  runListRoutes.get("/", requireWorkflowPlane, async (c) => {
10292
10821
  const nestId = c.req.param("nestId");
10293
10822
  const db = getDb();
@@ -10319,7 +10848,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
10319
10848
  }))
10320
10849
  });
10321
10850
  });
10322
- var runDetailRoutes = new Hono15();
10851
+ var runDetailRoutes = new Hono16();
10323
10852
  async function loadRunWithPermission(c, runId, required) {
10324
10853
  const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
10325
10854
  runId
@@ -10470,8 +10999,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
10470
10999
  });
10471
11000
 
10472
11001
  // src/workflow/schedule-routes.ts
10473
- import { Hono as Hono16 } from "hono";
10474
- import { v4 as uuid13 } from "uuid";
11002
+ import { Hono as Hono17 } from "hono";
11003
+ import { v4 as uuid14 } from "uuid";
10475
11004
  var RUNNABLE_TYPES3 = ["agent", "skill"];
10476
11005
  var MIN_EVERY = 5;
10477
11006
  var MAX_EVERY = 7 * 24 * 60;
@@ -10508,7 +11037,7 @@ function parseEvery(v) {
10508
11037
  }
10509
11038
  return n;
10510
11039
  }
10511
- var scheduleRoutes = new Hono16();
11040
+ var scheduleRoutes = new Hono17();
10512
11041
  scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
10513
11042
  const nestId = c.req.param("nestId");
10514
11043
  const rows = await getDb().all(
@@ -10549,7 +11078,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
10549
11078
  );
10550
11079
  }
10551
11080
  const now = (/* @__PURE__ */ new Date()).toISOString();
10552
- const id = `sch_${uuid13()}`;
11081
+ const id = `sch_${uuid14()}`;
10553
11082
  await db.run(
10554
11083
  `INSERT INTO schedules (id, nest_id, agent_node, every_minutes, enabled, last_run_at, max_runs, runs_created, created_by, created_at, updated_at)
10555
11084
  VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
@@ -10604,13 +11133,14 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
10604
11133
  });
10605
11134
 
10606
11135
  // src/notify/connector-routes.ts
10607
- import { Hono as Hono17 } from "hono";
10608
- import { v4 as uuid14 } from "uuid";
11136
+ import { Hono as Hono18 } from "hono";
11137
+ import { v4 as uuid15 } from "uuid";
10609
11138
  var KNOWN_EVENTS = [
10610
11139
  "review_requested",
10611
11140
  "review_approved",
10612
11141
  "review_rejected",
10613
- "run_failed"
11142
+ "run_failed",
11143
+ "mention"
10614
11144
  ];
10615
11145
  var CHANNELS = ["slack", "teams", "webhook"];
10616
11146
  function toResponse3(r) {
@@ -10651,7 +11181,7 @@ function parseUrl(v) {
10651
11181
  'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
10652
11182
  );
10653
11183
  }
10654
- var connectorRoutes = new Hono17();
11184
+ var connectorRoutes = new Hono18();
10655
11185
  connectorRoutes.get("/", async (c) => {
10656
11186
  const nestId = c.req.param("nestId");
10657
11187
  const rows = await getDb().all(
@@ -10670,7 +11200,7 @@ connectorRoutes.post("/", async (c) => {
10670
11200
  const url = parseUrl(body.url);
10671
11201
  const events = parseEvents(body.events);
10672
11202
  const now = (/* @__PURE__ */ new Date()).toISOString();
10673
- const id = `con_${uuid14()}`;
11203
+ const id = `con_${uuid15()}`;
10674
11204
  await getDb().run(
10675
11205
  `INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
10676
11206
  VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
@@ -10713,7 +11243,7 @@ connectorRoutes.delete("/:id", async (c) => {
10713
11243
  });
10714
11244
 
10715
11245
  // src/workflow/hook-routes.ts
10716
- import { Hono as Hono18 } from "hono";
11246
+ import { Hono as Hono19 } from "hono";
10717
11247
  import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
10718
11248
  var PRESETS = ["slack", "teams", "webhook"];
10719
11249
  var RUNNABLE_TYPES4 = ["agent", "skill"];
@@ -10736,7 +11266,7 @@ function toResponse4(c, r, opts = {}) {
10736
11266
  created_at: r.created_at
10737
11267
  };
10738
11268
  }
10739
- var hookNestRoutes = new Hono18();
11269
+ var hookNestRoutes = new Hono19();
10740
11270
  hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
10741
11271
  const nestId = c.req.param("nestId");
10742
11272
  const agent = c.req.query("agent");
@@ -10810,7 +11340,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
10810
11340
  function stripMentions(text) {
10811
11341
  return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
10812
11342
  }
10813
- var hookFireRoutes = new Hono18();
11343
+ var hookFireRoutes = new Hono19();
10814
11344
  hookFireRoutes.post("/:token", async (c) => {
10815
11345
  const token = c.req.param("token");
10816
11346
  if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
@@ -10912,9 +11442,9 @@ hookFireRoutes.post("/:token", async (c) => {
10912
11442
  });
10913
11443
 
10914
11444
  // src/definitions/routes.ts
10915
- import { Hono as Hono19 } from "hono";
10916
- import { v4 as uuid15 } from "uuid";
10917
- var definitionRoutes = new Hono19();
11445
+ import { Hono as Hono20 } from "hono";
11446
+ import { v4 as uuid16 } from "uuid";
11447
+ var definitionRoutes = new Hono20();
10918
11448
  definitionRoutes.get("/", async (c) => {
10919
11449
  const nestId = c.req.param("nestId");
10920
11450
  const db = getDb();
@@ -10993,7 +11523,7 @@ definitionRoutes.post("/", async (c) => {
10993
11523
  await applyUpdate(tx, existing.id);
10994
11524
  targetId = existing.id;
10995
11525
  } else {
10996
- targetId = uuid15();
11526
+ targetId = uuid16();
10997
11527
  wasCreated = true;
10998
11528
  await tx.run(
10999
11529
  `INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
@@ -11022,11 +11552,11 @@ definitionRoutes.delete("/:id", async (c) => {
11022
11552
  });
11023
11553
 
11024
11554
  // src/nests/asset-routes.ts
11025
- import { Hono as Hono20 } from "hono";
11026
- import { v4 as uuid16 } from "uuid";
11555
+ import { Hono as Hono21 } from "hono";
11556
+ import { v4 as uuid17 } from "uuid";
11027
11557
  import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
11028
11558
  import { join as join4 } from "path";
11029
- var assetRoutes = new Hono20();
11559
+ var assetRoutes = new Hono21();
11030
11560
  var CONTENT_TYPES = {
11031
11561
  png: "image/png",
11032
11562
  jpg: "image/jpeg",
@@ -11072,7 +11602,7 @@ assetRoutes.post("/", async (c) => {
11072
11602
  `${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
11073
11603
  );
11074
11604
  }
11075
- const name = `${uuid16()}.${ext === "jpeg" ? "jpg" : ext}`;
11605
+ const name = `${uuid17()}.${ext === "jpeg" ? "jpg" : ext}`;
11076
11606
  const dir = join4(resolveNestPath(nestId), "assets");
11077
11607
  await mkdir2(dir, { recursive: true });
11078
11608
  await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
@@ -11112,8 +11642,108 @@ assetRoutes.get("/:file", async (c) => {
11112
11642
  });
11113
11643
  });
11114
11644
 
11645
+ // src/auth/community-sites-routes.ts
11646
+ import { Hono as Hono22 } from "hono";
11647
+ var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
11648
+ var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
11649
+ var serializeSite = (site) => ({
11650
+ id: site.id,
11651
+ name: site.name,
11652
+ url: site.url,
11653
+ secret_set: !!site.secret,
11654
+ is_active: site.is_active,
11655
+ created_at: site.created_at,
11656
+ updated_at: site.updated_at
11657
+ });
11658
+ var communitySiteRoutes = new Hono22();
11659
+ communitySiteRoutes.get("/", async (c) => {
11660
+ if (!await serverAdminAllowed(c))
11661
+ return c.json({ error: "Only the server admin can view this." }, 403);
11662
+ const sites = await listCommunitySites();
11663
+ return c.json({ community_sites: sites.map(serializeSite) });
11664
+ });
11665
+ communitySiteRoutes.post("/", async (c) => {
11666
+ if (!await serverAdminAllowed(c))
11667
+ return c.json({ error: "Only the server admin can add a community site." }, 403);
11668
+ let body;
11669
+ try {
11670
+ body = await c.req.json();
11671
+ } catch {
11672
+ return c.json({ error: "Invalid JSON body" }, 400);
11673
+ }
11674
+ const name = typeof body.name === "string" ? body.name.trim() : "";
11675
+ const url = typeof body.url === "string" ? body.url.trim().replace(/\/$/, "") : "";
11676
+ const secret = typeof body.secret === "string" ? body.secret.trim() : "";
11677
+ const is_active = body.is_active === void 0 ? true : !!body.is_active;
11678
+ if (!name) return c.json({ error: "A name is required." }, 400);
11679
+ if (!url || !isHttpUrl(url))
11680
+ return c.json({ error: "A valid http(s) site URL is required." }, 400);
11681
+ if (!secret || secret.length < 16)
11682
+ return c.json({ error: "A secret of at least 16 characters is required." }, 400);
11683
+ try {
11684
+ const site = await createCommunitySite({ name, url, secret, is_active });
11685
+ await loadActiveCommunitySites();
11686
+ return c.json(serializeSite(site), 201);
11687
+ } catch (err) {
11688
+ const msg = err instanceof Error ? err.message : String(err);
11689
+ if (/unique|duplicate/i.test(msg))
11690
+ return c.json({ error: "A community site with this URL already exists." }, 409);
11691
+ throw err;
11692
+ }
11693
+ });
11694
+ communitySiteRoutes.patch("/:id", async (c) => {
11695
+ if (!await serverAdminAllowed(c))
11696
+ return c.json({ error: "Only the server admin can edit a community site." }, 403);
11697
+ const id = c.req.param("id");
11698
+ let body;
11699
+ try {
11700
+ body = await c.req.json();
11701
+ } catch {
11702
+ return c.json({ error: "Invalid JSON body" }, 400);
11703
+ }
11704
+ const patch = {};
11705
+ if ("name" in body) {
11706
+ const v = typeof body.name === "string" ? body.name.trim() : "";
11707
+ if (!v) return c.json({ error: "A name is required." }, 400);
11708
+ patch.name = v;
11709
+ }
11710
+ if ("url" in body) {
11711
+ const v = typeof body.url === "string" ? body.url.trim().replace(/\/$/, "") : "";
11712
+ if (!v || !isHttpUrl(v))
11713
+ return c.json({ error: "A valid http(s) site URL is required." }, 400);
11714
+ patch.url = v;
11715
+ }
11716
+ if ("secret" in body) {
11717
+ const v = typeof body.secret === "string" ? body.secret.trim() : "";
11718
+ if (!v || v.length < 16)
11719
+ return c.json({ error: "A secret of at least 16 characters is required." }, 400);
11720
+ patch.secret = v;
11721
+ }
11722
+ if ("is_active" in body) patch.is_active = !!body.is_active;
11723
+ try {
11724
+ const site = await updateCommunitySite(id, patch);
11725
+ if (!site) return c.json({ error: "Community site not found." }, 404);
11726
+ await loadActiveCommunitySites();
11727
+ return c.json(serializeSite(site));
11728
+ } catch (err) {
11729
+ const msg = err instanceof Error ? err.message : String(err);
11730
+ if (/unique|duplicate/i.test(msg))
11731
+ return c.json({ error: "A community site with this URL already exists." }, 409);
11732
+ throw err;
11733
+ }
11734
+ });
11735
+ communitySiteRoutes.delete("/:id", async (c) => {
11736
+ if (!await serverAdminAllowed(c))
11737
+ return c.json({ error: "Only the server admin can remove a community site." }, 403);
11738
+ const id = c.req.param("id");
11739
+ const removed = await deleteCommunitySite(id);
11740
+ if (!removed) return c.json({ error: "Community site not found." }, 404);
11741
+ await loadActiveCommunitySites();
11742
+ return c.json({ ok: true, id });
11743
+ });
11744
+
11115
11745
  // src/mcp/server-routes.ts
11116
- import { Hono as Hono21 } from "hono";
11746
+ import { Hono as Hono23 } from "hono";
11117
11747
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
11118
11748
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
11119
11749
  import { z as z2 } from "zod";
@@ -11173,7 +11803,9 @@ async function buildNestIndex(userId, nestScope) {
11173
11803
  let document_count = null;
11174
11804
  try {
11175
11805
  const { storage } = await engineCache.get(n.id);
11176
- document_count = (await storage.discoverDocuments()).length;
11806
+ document_count = (await storage.discoverDocuments()).filter(
11807
+ (d) => !isDerivedAnnotationsNode(d.id)
11808
+ ).length;
11177
11809
  } catch {
11178
11810
  }
11179
11811
  return {
@@ -11309,6 +11941,22 @@ var SERVER_TOOL_DEFINITIONS = [
11309
11941
  required: ["nest", "selector"]
11310
11942
  }
11311
11943
  },
11944
+ {
11945
+ // Read-only, so it belongs on this endpoint: an agent reading a nest's
11946
+ // documents across nests also needs the feedback left on them. Writing a
11947
+ // comment stays on the per-nest endpoint with the other write tools.
11948
+ name: "context_comments",
11949
+ description: "Get the review comments/annotations on a document in a nest \u2014 anchored quotes, threaded replies, and open/resolved status. Use to pull human feedback into context before acting on a document.",
11950
+ inputSchema: {
11951
+ type: "object",
11952
+ properties: {
11953
+ nest: { type: "string", description: "Nest name or id (see nest_index)" },
11954
+ title: { type: "string", description: "Title of the node" },
11955
+ id: { type: "string", description: "ID of the node" }
11956
+ },
11957
+ required: ["nest"]
11958
+ }
11959
+ },
11312
11960
  {
11313
11961
  name: "nest_create",
11314
11962
  description: "Create a new nest owned by the caller. Returns the new nest's id \u2014 target it with the other tools afterwards.",
@@ -11421,7 +12069,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
11421
12069
  }
11422
12070
  return server;
11423
12071
  }
11424
- var serverMcpRoutes = new Hono21();
12072
+ var serverMcpRoutes = new Hono23();
11425
12073
  serverMcpRoutes.all("/", async (c) => {
11426
12074
  const userId = c.get("userId");
11427
12075
  const userEmail = await getUserEmail3(userId);
@@ -11443,7 +12091,7 @@ serverMcpRoutes.all("/", async (c) => {
11443
12091
  await server.server.close();
11444
12092
  }
11445
12093
  });
11446
- var nestIndexRoutes = new Hono21();
12094
+ var nestIndexRoutes = new Hono23();
11447
12095
  nestIndexRoutes.get("/", async (c) => {
11448
12096
  const userId = c.get("userId");
11449
12097
  const nests = await buildNestIndex(userId, c.get("nestScope"));
@@ -11582,14 +12230,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
11582
12230
  through steward review before they become AI-visible.
11583
12231
  `;
11584
12232
  }
11585
- var llmsTxtRoutes = new Hono21();
12233
+ var llmsTxtRoutes = new Hono23();
11586
12234
  llmsTxtRoutes.get("/", (c) => {
11587
12235
  const md = renderLlmsTxt(requestBaseUrl(c.req.url));
11588
12236
  return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
11589
12237
  });
11590
12238
 
11591
12239
  // src/governance/routes.ts
11592
- import { Hono as Hono22 } from "hono";
12240
+ import { Hono as Hono24 } from "hono";
11593
12241
 
11594
12242
  // src/governance/feed-service.ts
11595
12243
  var SNIPPET_CHARS = 200;
@@ -11720,7 +12368,7 @@ async function getNestFeed(params) {
11720
12368
  }
11721
12369
 
11722
12370
  // src/governance/comment-service.ts
11723
- import { v4 as uuid17 } from "uuid";
12371
+ import { v4 as uuid18 } from "uuid";
11724
12372
  async function createComment(params) {
11725
12373
  const db = getDb();
11726
12374
  const body = (params.body ?? "").trim();
@@ -11736,7 +12384,7 @@ async function createComment(params) {
11736
12384
  throw new Error("Parent comment not found on this node");
11737
12385
  }
11738
12386
  }
11739
- const id = uuid17();
12387
+ const id = uuid18();
11740
12388
  await db.run(
11741
12389
  `INSERT INTO comments
11742
12390
  (id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
@@ -12021,7 +12669,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
12021
12669
  if (Number.isNaN(n) || n < 1) return def;
12022
12670
  return Math.min(n, max);
12023
12671
  }
12024
- var governanceRoutes = new Hono22();
12672
+ var governanceRoutes = new Hono24();
12025
12673
  governanceRoutes.get("/stewards", async (c) => {
12026
12674
  const nestId = c.req.param("nestId");
12027
12675
  const scope = c.req.query("scope");
@@ -12388,7 +13036,7 @@ governanceRoutes.get("/activity", async (c) => {
12388
13036
  const activity = await getActivity({ nestId, limit });
12389
13037
  return c.json({ activity });
12390
13038
  });
12391
- var governanceNodeRoutes = new Hono22();
13039
+ var governanceNodeRoutes = new Hono24();
12392
13040
  governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
12393
13041
  const nestId = c.req.param("nestId");
12394
13042
  const nodeId = c.req.param("nodeId");
@@ -12520,8 +13168,9 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
12520
13168
  }
12521
13169
  const body = await c.req.json();
12522
13170
  const author = await getUserEmail4(c);
13171
+ let comment;
12523
13172
  try {
12524
- const comment = await createComment({
13173
+ comment = await createComment({
12525
13174
  nestId,
12526
13175
  nodeId,
12527
13176
  author,
@@ -12530,12 +13179,19 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
12530
13179
  anchor: body.anchor,
12531
13180
  parentId: body.parentId
12532
13181
  });
12533
- return c.json({ comment }, 201);
12534
13182
  } catch (err) {
12535
13183
  throw new ValidationError(
12536
13184
  err instanceof Error ? err.message : String(err)
12537
13185
  );
12538
13186
  }
13187
+ await notifyCommentMentions({
13188
+ nestId,
13189
+ nodeId,
13190
+ body: comment.body,
13191
+ actorEmail: author,
13192
+ baseUrl: requestBaseUrl(c.req.url)
13193
+ });
13194
+ return c.json({ comment }, 201);
12539
13195
  });
12540
13196
  governanceNodeRoutes.post(
12541
13197
  "/:nodeId{.+}/comments/:commentId/resolve",
@@ -12953,7 +13609,7 @@ async function canApproveInNest(nestId, email) {
12953
13609
  return ok;
12954
13610
  }
12955
13611
  function createApp() {
12956
- const app = new Hono23({ router: new LinearRouter() });
13612
+ const app = new Hono25({ router: new LinearRouter() });
12957
13613
  const corsOrigins = config.CORS_ORIGINS;
12958
13614
  app.use(
12959
13615
  "*",
@@ -13134,7 +13790,7 @@ function createApp() {
13134
13790
  return c.json({ error: msg }, 500);
13135
13791
  }
13136
13792
  });
13137
- const serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
13793
+ const serverAdminAllowed2 = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
13138
13794
  app.use("/admin/settings", flexAuthMiddleware);
13139
13795
  app.use("/admin/settings/*", flexAuthMiddleware);
13140
13796
  const currentServerSettings = () => ({
@@ -13163,6 +13819,7 @@ function createApp() {
13163
13819
  oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
13164
13820
  oidc_auto_provision: config.OIDC_AUTO_PROVISION,
13165
13821
  oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
13822
+ people_suggest_all_users: config.PEOPLE_SUGGEST_ALL_USERS,
13166
13823
  // Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
13167
13824
  // presence is reported, never the value (same discipline as the OIDC secret).
13168
13825
  sso_token_exchange_enabled: config.SSO_TOKEN_EXCHANGE_ENABLED,
@@ -13187,12 +13844,12 @@ function createApp() {
13187
13844
  notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
13188
13845
  });
13189
13846
  app.get("/admin/settings", async (c) => {
13190
- if (!await serverAdminAllowed(c))
13847
+ if (!await serverAdminAllowed2(c))
13191
13848
  return c.json({ error: "Only the server admin can view this." }, 403);
13192
13849
  return c.json(currentServerSettings());
13193
13850
  });
13194
13851
  app.post("/admin/settings/test-notification", async (c) => {
13195
- if (!await serverAdminAllowed(c))
13852
+ if (!await serverAdminAllowed2(c))
13196
13853
  return c.json({ error: "Only the server admin can do this." }, 403);
13197
13854
  let body;
13198
13855
  try {
@@ -13206,7 +13863,7 @@ function createApp() {
13206
13863
  return result.ok ? c.json({ ok: true }) : c.json({ error: result.error }, 502);
13207
13864
  });
13208
13865
  app.patch("/admin/settings", async (c) => {
13209
- if (!await serverAdminAllowed(c))
13866
+ if (!await serverAdminAllowed2(c))
13210
13867
  return c.json({ error: "Only the server admin can change this." }, 403);
13211
13868
  let body;
13212
13869
  try {
@@ -13383,6 +14040,12 @@ function createApp() {
13383
14040
  value: body.oidc_department_tagging ? "true" : null
13384
14041
  });
13385
14042
  }
14043
+ if ("people_suggest_all_users" in body) {
14044
+ pending.push({
14045
+ name: "PEOPLE_SUGGEST_ALL_USERS",
14046
+ value: body.people_suggest_all_users ? "true" : null
14047
+ });
14048
+ }
13386
14049
  if ("sso_token_exchange_enabled" in body) {
13387
14050
  pending.push({
13388
14051
  name: "SSO_TOKEN_EXCHANGE_ENABLED",
@@ -13551,7 +14214,7 @@ function createApp() {
13551
14214
  });
13552
14215
  app.use("/admin/trace", flexAuthMiddleware);
13553
14216
  app.get("/admin/trace", async (c) => {
13554
- if (!await serverAdminAllowed(c))
14217
+ if (!await serverAdminAllowed2(c))
13555
14218
  return c.json({ error: "Only the server admin can view this." }, 403);
13556
14219
  const limitRaw = Number(c.req.query("limit"));
13557
14220
  const offsetRaw = Number(c.req.query("offset"));
@@ -13569,7 +14232,7 @@ function createApp() {
13569
14232
  app.use("/admin/super-admins", flexAuthMiddleware);
13570
14233
  app.use("/admin/super-admins/*", flexAuthMiddleware);
13571
14234
  app.get("/admin/super-admins", async (c) => {
13572
- if (!await serverAdminAllowed(c))
14235
+ if (!await serverAdminAllowed2(c))
13573
14236
  return c.json({ error: "Only the server admin can view this." }, 403);
13574
14237
  const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
13575
14238
  const byEmail = /* @__PURE__ */ new Map();
@@ -13595,7 +14258,7 @@ function createApp() {
13595
14258
  return c.json({ super_admins: [...byEmail.values()] });
13596
14259
  });
13597
14260
  app.post("/admin/super-admins", async (c) => {
13598
- if (!await serverAdminAllowed(c))
14261
+ if (!await serverAdminAllowed2(c))
13599
14262
  return c.json({ error: "Only the server admin can grant superadmin." }, 403);
13600
14263
  let body;
13601
14264
  try {
@@ -13640,7 +14303,7 @@ function createApp() {
13640
14303
  return c.json({ email, source: "granted", granted_by: grantedBy }, 201);
13641
14304
  });
13642
14305
  app.delete("/admin/super-admins/:email", async (c) => {
13643
- if (!await serverAdminAllowed(c))
14306
+ if (!await serverAdminAllowed2(c))
13644
14307
  return c.json({ error: "Only the server admin can revoke superadmin." }, 403);
13645
14308
  const email = c.req.param("email").trim().toLowerCase();
13646
14309
  const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
@@ -13681,9 +14344,14 @@ function createApp() {
13681
14344
  await loadGrantedSuperAdmins();
13682
14345
  return c.json({ ok: true, email });
13683
14346
  });
14347
+ app.use("/admin/community-sites", flexAuthMiddleware);
14348
+ app.use("/admin/community-sites/*", flexAuthMiddleware);
14349
+ app.route("/admin/community-sites", communitySiteRoutes);
13684
14350
  app.use("/teams/*", flexAuthMiddleware);
13685
14351
  app.use("/teams", flexAuthMiddleware);
13686
14352
  app.route("/teams", teamRoutes);
14353
+ app.use("/people/*", flexAuthMiddleware);
14354
+ app.route("/people", peopleRoutes);
13687
14355
  app.use("/me/*", flexAuthMiddleware);
13688
14356
  app.get("/me/work", async (c) => {
13689
14357
  const userId = c.get("userId");
@@ -13827,7 +14495,9 @@ function createApp() {
13827
14495
  for (const nest of visibleNests) {
13828
14496
  try {
13829
14497
  const { storage } = await engineCache.get(nest.id);
13830
- const docs = await storage.discoverDocuments();
14498
+ const docs = (await storage.discoverDocuments()).filter(
14499
+ (d) => !isDerivedAnnotationsNode(d.id)
14500
+ );
13831
14501
  documents += (await filterAccessible(nest.id, userId, userEmail, docs)).length;
13832
14502
  } catch {
13833
14503
  }
@@ -13882,7 +14552,7 @@ function createApp() {
13882
14552
  app.route("/index", nestIndexRoutes);
13883
14553
  app.use("/mcp", flexAuthWithPromptowlTicket);
13884
14554
  app.route("/mcp", serverMcpRoutes);
13885
- const nestsApp = new Hono23();
14555
+ const nestsApp = new Hono25();
13886
14556
  nestsApp.use("*", async (c, next) => {
13887
14557
  const parts = nestPathParts(c.req.path);
13888
14558
  const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
@@ -14414,7 +15084,7 @@ async function repairGraftedVersionChains(db) {
14414
15084
  }
14415
15085
 
14416
15086
  // src/workflow/scheduler.ts
14417
- import { v4 as uuid18 } from "uuid";
15087
+ import { v4 as uuid19 } from "uuid";
14418
15088
  function isDue(s, now) {
14419
15089
  if (!s.last_run_at) return true;
14420
15090
  const last = Date.parse(s.last_run_at);
@@ -14442,7 +15112,7 @@ async function schedulerTick(now = Date.now()) {
14442
15112
  );
14443
15113
  continue;
14444
15114
  }
14445
- const id = `run_${uuid18()}`;
15115
+ const id = `run_${uuid19()}`;
14446
15116
  const nowIso2 = new Date(now).toISOString();
14447
15117
  await db.run(
14448
15118
  `INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
@@ -14512,6 +15182,11 @@ async function main() {
14512
15182
  } catch (err) {
14513
15183
  console.error("[access] failed to load DB-granted superadmins:", err);
14514
15184
  }
15185
+ try {
15186
+ await loadActiveCommunitySites();
15187
+ } catch (err) {
15188
+ console.error("[sso] failed to load community sites:", err);
15189
+ }
14515
15190
  const license = await validateLicense();
14516
15191
  if (!license.valid) {
14517
15192
  console.warn(`