@promptowl/contextnest-community 1.15.0 → 1.17.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.
@@ -8,7 +8,7 @@ import {
8
8
  config,
9
9
  getDb,
10
10
  isEmailish
11
- } from "./chunk-BCFKLY4H.js";
11
+ } from "./chunk-JMJLNEXE.js";
12
12
  import {
13
13
  ANON_USER_ID
14
14
  } from "./chunk-SLTQACJW.js";
@@ -600,7 +600,7 @@ var STATUS_META = {
600
600
  },
601
601
  deletion_declined: {
602
602
  subjectWord: "kept",
603
- label: "Deletion declined",
603
+ label: "Deletion rejected",
604
604
  color: "#d97706"
605
605
  },
606
606
  deletion_deleted: {
@@ -683,16 +683,16 @@ var TEMPLATES = {
683
683
  heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
684
684
  detailsHeader: "Document details",
685
685
  linkLabel: "Review the request",
686
- lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or decline the request from the nest.`,
687
- lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or decline the request from the nest.`,
686
+ lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or reject the request from the nest.`,
687
+ lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or reject the request from the nest.`,
688
688
  rows: governanceRows
689
689
  },
690
690
  deletion_declined: {
691
- heading: (x) => `Your deletion request for ${x.title} was declined`,
691
+ heading: (x) => `Your deletion request for ${x.title} was rejected`,
692
692
  detailsHeader: "Document details",
693
693
  linkLabel: "Open document",
694
- lineT: (x) => `Your request to delete ${x.title} was declined${x.byT}${x.noteT}. The document stays.`,
695
- lineH: (x) => `Your request to delete ${x.doc} was declined${x.byH}${x.noteH}. The document stays.`,
694
+ lineT: (x) => `Your request to delete ${x.title} was rejected${x.byT}${x.noteT}. The document stays.`,
695
+ lineH: (x) => `Your request to delete ${x.doc} was rejected${x.byH}${x.noteH}. The document stays.`,
696
696
  rows: governanceRows
697
697
  },
698
698
  deletion_deleted: {
@@ -973,74 +973,17 @@ async function sendEmailToRecipient(to, details) {
973
973
  }
974
974
 
975
975
  // src/nests/service.ts
976
- import { rmSync as rmSync2, mkdirSync } from "fs";
976
+ import { mkdirSync } from "fs";
977
977
  import { v4 as uuid2 } from "uuid";
978
978
  import { NestStorage as NestStorage3 } from "@promptowl/contextnest-engine";
979
979
 
980
- // src/fs/vault-import.ts
981
- import { rmSync } from "fs";
982
- import { writeFile, mkdir } from "fs/promises";
983
- import { join as join2, dirname, isAbsolute } from "path";
984
- function isSkippedSegment(segment) {
985
- return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
986
- }
987
- var ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".versions", ".context"]);
988
- function safeSegments(rel) {
989
- if (!rel || isAbsolute(rel)) return null;
990
- const segs = rel.split(/[\\/]/).filter(Boolean);
991
- if (!segs.length) return null;
992
- for (const s of segs) {
993
- if (s === "..") return null;
994
- if (!ALLOWED_DOT_DIRS.has(s) && isSkippedSegment(s)) return null;
995
- }
996
- return segs;
997
- }
998
- var WRITE_CONCURRENCY = 32;
999
- async function writeImportedFiles(dest, files) {
1000
- const targets = [];
1001
- const skipped = [];
1002
- for (const f of files) {
1003
- const segs = safeSegments(f?.path || "");
1004
- if (!segs) {
1005
- skipped.push(f?.path || "(empty)");
1006
- continue;
1007
- }
1008
- targets.push({ full: join2(dest, ...segs), content: f.content ?? "" });
1009
- }
1010
- const dirs = new Set(targets.map((t) => dirname(t.full)));
1011
- await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })));
1012
- let written = 0;
1013
- for (let i = 0; i < targets.length; i += WRITE_CONCURRENCY) {
1014
- const batch = targets.slice(i, i + WRITE_CONCURRENCY);
1015
- await Promise.all(
1016
- batch.map(async (t) => {
1017
- await writeFile(t.full, t.content, "utf-8");
1018
- written++;
1019
- })
1020
- );
1021
- }
1022
- console.log(
1023
- `[import] writeImportedFiles: received=${files.length} written=${written} skipped=${skipped.length}`
1024
- );
1025
- if (skipped.length) {
1026
- console.log("[import] skipped (unsafe/hidden path):", skipped.join(", "));
1027
- }
1028
- return written;
1029
- }
1030
- function discardImportDir(dest) {
1031
- try {
1032
- rmSync(dest, { recursive: true, force: true });
1033
- } catch {
1034
- }
1035
- }
1036
-
1037
980
  // src/shared/paths.ts
1038
- import { join as join3 } from "path";
981
+ import { join as join2 } from "path";
1039
982
  function nestStorageRoot() {
1040
983
  return config.NEST_STORAGE_ROOT;
1041
984
  }
1042
985
  function resolveNestPath(nestId) {
1043
- return join3(nestStorageRoot(), nestId);
986
+ return join2(nestStorageRoot(), nestId);
1044
987
  }
1045
988
 
1046
989
  // src/governance/teams-service.ts
@@ -1050,13 +993,32 @@ import { v4 as uuid } from "uuid";
1050
993
  import {
1051
994
  NestStorage as NestStorage2,
1052
995
  GraphQueryEngine,
1053
- VersionManager
996
+ VersionManager,
997
+ DocumentNotFoundError
1054
998
  } from "@promptowl/contextnest-engine";
999
+ import { createEngineApi } from "@promptowl/contextnest-engine/api";
1055
1000
 
1056
1001
  // src/nodes/flat-storage.ts
1057
- import { readdirSync } from "fs";
1058
- import { join as join4, relative, sep } from "path";
1002
+ import { readdir } from "fs/promises";
1003
+ import { join as join3, relative, sep } from "path";
1059
1004
  import { NestStorage } from "@promptowl/contextnest-engine";
1005
+
1006
+ // src/fs/vault-import.ts
1007
+ function isSkippedSegment(segment) {
1008
+ return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
1009
+ }
1010
+
1011
+ // src/shared/batch.ts
1012
+ var IO_CONCURRENCY = 32;
1013
+ async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
1014
+ const out = [];
1015
+ for (let i = 0; i < items.length; i += concurrency) {
1016
+ out.push(...await Promise.all(items.slice(i, i + concurrency).map(fn)));
1017
+ }
1018
+ return out;
1019
+ }
1020
+
1021
+ // src/nodes/flat-storage.ts
1060
1022
  var META_FILES = /* @__PURE__ */ new Set([
1061
1023
  "INDEX.md",
1062
1024
  "CONTEXT.md",
@@ -1070,7 +1032,7 @@ var FlatNestStorage = class extends NestStorage {
1070
1032
  const root = this.root;
1071
1033
  let entries;
1072
1034
  try {
1073
- entries = readdirSync(root, { recursive: true, withFileTypes: true });
1035
+ entries = await readdir(root, { recursive: true, withFileTypes: true });
1074
1036
  } catch (err) {
1075
1037
  console.error("[FlatNestStorage] cannot read folder", root, err);
1076
1038
  return [];
@@ -1080,33 +1042,121 @@ var FlatNestStorage = class extends NestStorage {
1080
1042
  if (!e.isFile() || !e.name.endsWith(".md")) continue;
1081
1043
  if (META_FILES.has(e.name)) continue;
1082
1044
  const dir = e.parentPath ?? e.path ?? root;
1083
- const rel = relative(root, join4(dir, e.name)).split(sep).join("/");
1045
+ const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
1084
1046
  if (rel.split("/").some(isSkippedSegment)) continue;
1085
1047
  ids.push(rel.replace(/\.md$/, ""));
1086
1048
  }
1087
- const nodes = [];
1088
- for (const id of ids.sort()) {
1049
+ ids.sort();
1050
+ const read = await mapBatched(ids, async (id) => {
1089
1051
  try {
1090
- nodes.push(await this.readDocument(id));
1052
+ return await this.readDocument(id);
1091
1053
  } catch (err) {
1092
1054
  console.error("[FlatNestStorage] skipped unreadable doc", id, err);
1055
+ return null;
1093
1056
  }
1094
- }
1095
- return nodes;
1057
+ });
1058
+ return read.filter((n) => n !== null);
1096
1059
  }
1097
1060
  };
1098
1061
 
1099
1062
  // src/nodes/engine.ts
1063
+ var DISCOVERY_TTL_MS = 3e4;
1064
+ var isLive = (node) => node.frontmatter.status !== "rejected";
1065
+ var DERIVED_VAULT_FILES = /* @__PURE__ */ new Set([
1066
+ "INDEX.md",
1067
+ "CONTEXT.md",
1068
+ "CLAUDE.md",
1069
+ "GEMINI.md",
1070
+ "AGENTS.md",
1071
+ "README.md"
1072
+ ]);
1073
+ function withDiscoveryCache(storage) {
1074
+ const entries = /* @__PURE__ */ new Map();
1075
+ const discover = storage.discoverDocuments.bind(storage);
1076
+ const readDoc = storage.readDocument.bind(storage);
1077
+ const filtersRetired = !(storage instanceof FlatNestStorage);
1078
+ let generation = 0;
1079
+ storage.discoverDocuments = async (options = {}) => {
1080
+ const key = options.includeRetired || options.includeSuperseded ? "all" : "live";
1081
+ const hit = entries.get(key);
1082
+ if (hit && Date.now() - hit.ts < DISCOVERY_TTL_MS) return hit.docs;
1083
+ const all = entries.get("all");
1084
+ if (all && Date.now() - all.ts < DISCOVERY_TTL_MS) {
1085
+ const docs2 = filtersRetired ? all.docs.filter(isLive) : all.docs;
1086
+ entries.set("live", { docs: docs2, ts: all.ts });
1087
+ return docs2;
1088
+ }
1089
+ const startedAt = generation;
1090
+ const crawled = await discover({ ...options, includeRetired: true });
1091
+ const docs = key === "all" || !filtersRetired ? crawled : crawled.filter(isLive);
1092
+ if (generation === startedAt) {
1093
+ const ts = Date.now();
1094
+ entries.set("all", { docs: crawled, ts });
1095
+ entries.set(key, { docs, ts });
1096
+ }
1097
+ return docs;
1098
+ };
1099
+ const invalidate = () => {
1100
+ generation++;
1101
+ entries.clear();
1102
+ };
1103
+ const patch = async (id) => {
1104
+ generation++;
1105
+ let node = null;
1106
+ try {
1107
+ node = await readDoc(id);
1108
+ } catch (err) {
1109
+ if (!(err instanceof DocumentNotFoundError)) {
1110
+ invalidate();
1111
+ return;
1112
+ }
1113
+ node = null;
1114
+ }
1115
+ for (const [key, entry] of entries) {
1116
+ const docs = entry.docs.filter((d) => d.id !== id);
1117
+ if (node && (key === "all" || isLive(node))) {
1118
+ const at = docs.findIndex((d) => d.id > id);
1119
+ docs.splice(at === -1 ? docs.length : at, 0, node);
1120
+ }
1121
+ entry.docs = docs;
1122
+ }
1123
+ };
1124
+ const wrap = (name, after) => {
1125
+ const write = storage[name].bind(storage);
1126
+ storage[name] = async (...args) => {
1127
+ try {
1128
+ return await write(...args);
1129
+ } finally {
1130
+ try {
1131
+ await after(args);
1132
+ } catch {
1133
+ invalidate();
1134
+ }
1135
+ }
1136
+ };
1137
+ };
1138
+ wrap("writeDocument", (args) => patch(String(args[0])));
1139
+ wrap("deleteDocument", (args) => patch(String(args[0])));
1140
+ wrap("init", () => invalidate());
1141
+ wrap("writeVaultFile", (args) => {
1142
+ const rel = String(args[0] ?? "");
1143
+ const base = rel.split(/[/\\]/).pop() || "";
1144
+ if (rel.endsWith(".md") && !DERIVED_VAULT_FILES.has(base)) invalidate();
1145
+ });
1146
+ return { storage, invalidate };
1147
+ }
1100
1148
  var NestEngineCache = class {
1101
1149
  cache = /* @__PURE__ */ new Map();
1102
1150
  async get(nestId) {
1103
1151
  let engine = this.cache.get(nestId);
1104
1152
  if (!engine) {
1105
1153
  const nestPath = resolveNestPath(nestId);
1106
- const storage = await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath);
1154
+ const { storage, invalidate } = withDiscoveryCache(
1155
+ await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath)
1156
+ );
1107
1157
  const query = new GraphQueryEngine(storage);
1108
1158
  const versions = new VersionManager(storage);
1109
- engine = { storage, query, versions };
1159
+ engine = { storage, query, versions, invalidateDiscovery: invalidate };
1110
1160
  this.cache.set(nestId, engine);
1111
1161
  }
1112
1162
  return engine;
@@ -1116,6 +1166,11 @@ var NestEngineCache = class {
1116
1166
  }
1117
1167
  };
1118
1168
  var engineCache = new NestEngineCache();
1169
+ var engineApi = createEngineApi();
1170
+ async function opContext(nestId, actor, onProgress) {
1171
+ const { storage, query, versions } = await engineCache.get(nestId);
1172
+ return { storage, query, versions, actor, onProgress };
1173
+ }
1119
1174
 
1120
1175
  // src/governance/title-resolver.ts
1121
1176
  async function buildTitleMap(nestId) {
@@ -1601,14 +1656,18 @@ async function removeTeamRefs(tx, teamId) {
1601
1656
  }
1602
1657
  async function resolveTeamRolesForUser(nestId, userId) {
1603
1658
  const refs = await listNestTeams(nestId).catch(() => []);
1659
+ return resolveTeamRolesFromShared(refs, userId);
1660
+ }
1661
+ async function resolveTeamRolesFromShared(refs, userId) {
1604
1662
  if (refs.length === 0) return [];
1605
- const db = getDb();
1663
+ const ids = [...new Set(refs.map((r) => r.teamId).filter(Boolean))];
1664
+ if (ids.length === 0) return [];
1665
+ const rows = await getDb().all(
1666
+ `SELECT members FROM teams WHERE id IN (${ids.map(() => "?").join(",")})`,
1667
+ ids
1668
+ );
1606
1669
  const roles = /* @__PURE__ */ new Set();
1607
- for (const ref of refs) {
1608
- const row = await db.get("SELECT members FROM teams WHERE id = ?", [
1609
- ref.teamId
1610
- ]);
1611
- if (!row) continue;
1670
+ for (const row of rows) {
1612
1671
  const member = parseMembers(row.members).find((m) => m.userId === userId);
1613
1672
  if (member) roles.add(member.role);
1614
1673
  }
@@ -1674,71 +1733,91 @@ var PERMISSION_LEVELS = {
1674
1733
  admin: 3,
1675
1734
  owner: 4
1676
1735
  };
1677
- async function resolveNestPermission(nestId, userId) {
1678
- const db = getDb();
1679
- const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
1680
- if (!nest) return "none";
1681
- if (nest.user_id === userId) return "owner";
1682
- if (nest.user_id === ANON_USER_ID && await isLicenseAdminUserId(userId)) {
1683
- return "owner";
1684
- }
1685
- if (await isLicenseAdminUserId(userId)) return "admin";
1686
- const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
1687
- if (caller?.email && isSuperAdmin(caller.email)) {
1688
- return "admin";
1689
- }
1690
- const higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1691
- let best = "none";
1692
- const directGrant = await db.get(
1693
- "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
1694
- [nestId, userId]
1695
- );
1696
- if (directGrant) best = higher(best, directGrant.permission);
1697
- if (best === "admin") return best;
1698
- for (const role of await resolveTeamRolesForUser(nestId, userId)) {
1699
- best = higher(best, teamRoleToPermission(role));
1700
- }
1701
- if (best === "admin") return best;
1702
- const stewardGrant = await db.get(
1703
- `SELECT 1 FROM stewards s
1704
- JOIN users u ON u.id = ?
1705
- WHERE s.nest_id = ? AND s.is_active = 1
1706
- AND LOWER(s.user_email) = LOWER(u.email)
1707
- LIMIT 1`,
1708
- [userId, nestId]
1736
+ var higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1737
+ async function resolveNestAccess(nestId, userId) {
1738
+ const row = await getDb().get(
1739
+ `SELECT n.user_id AS owner_id,
1740
+ n.visibility AS visibility,
1741
+ n.shared_teams AS shared_teams,
1742
+ u.email AS caller_email,
1743
+ nc.permission AS collab_permission,
1744
+ (SELECT 1 FROM stewards s
1745
+ WHERE s.nest_id = n.id AND s.is_active = 1
1746
+ AND LOWER(s.user_email) = LOWER(u.email)
1747
+ LIMIT 1) AS steward_hit,
1748
+ (SELECT 1 FROM grants g
1749
+ WHERE g.nest_id = n.id AND g.user_id = ?
1750
+ LIMIT 1) AS grant_hit
1751
+ FROM nests n
1752
+ LEFT JOIN users u ON u.id = ?
1753
+ LEFT JOIN nest_collaborators nc
1754
+ ON nc.nest_id = n.id AND nc.user_id = ?
1755
+ WHERE n.id = ?`,
1756
+ [userId, userId, userId, nestId]
1709
1757
  );
1710
- if (stewardGrant) best = higher(best, "read");
1711
- if (nest.visibility === "public") best = higher(best, "read");
1712
- return best;
1758
+ const absent = {
1759
+ exists: false,
1760
+ ownerId: null,
1761
+ visibility: null,
1762
+ callerEmail: null,
1763
+ collaboratorPermission: null,
1764
+ isSteward: false,
1765
+ hasGrant: false,
1766
+ permission: "none",
1767
+ isPublicReader: false
1768
+ };
1769
+ if (!row) return absent;
1770
+ const base = {
1771
+ exists: true,
1772
+ ownerId: row.owner_id,
1773
+ visibility: row.visibility,
1774
+ callerEmail: row.caller_email ?? null,
1775
+ collaboratorPermission: row.collab_permission ?? null,
1776
+ // Postgres returns integer 1, SQLite returns 1 — both truthy, neither a
1777
+ // boolean, so normalize rather than leaking the driver's shape.
1778
+ isSteward: !!row.steward_hit,
1779
+ hasGrant: !!row.grant_hit
1780
+ };
1781
+ if (row.owner_id === userId) {
1782
+ return { ...base, permission: "owner", isPublicReader: false };
1783
+ }
1784
+ const email = row.caller_email ?? null;
1785
+ const licenseAdmin = isLicenseAdminEmail(email);
1786
+ if (row.owner_id === ANON_USER_ID && licenseAdmin) {
1787
+ return { ...base, permission: "owner", isPublicReader: false };
1788
+ }
1789
+ if (licenseAdmin || email && isSuperAdmin(email)) {
1790
+ return { ...base, permission: "admin", isPublicReader: false };
1791
+ }
1792
+ let best = higher("none", row.collab_permission);
1793
+ if (best !== "admin") {
1794
+ const teamRoles = await resolveTeamRolesFromShared(
1795
+ parseSharedTeams(row.shared_teams),
1796
+ userId
1797
+ );
1798
+ for (const role of teamRoles) {
1799
+ best = higher(best, teamRoleToPermission(role));
1800
+ }
1801
+ if (teamRoles.length > 0) {
1802
+ return { ...base, permission: best, isPublicReader: false };
1803
+ }
1804
+ }
1805
+ if (best === "admin") {
1806
+ return { ...base, permission: best, isPublicReader: false };
1807
+ }
1808
+ if (base.isSteward) best = higher(best, "read");
1809
+ if (row.visibility === "public") best = higher(best, "read");
1810
+ const isPublicReader2 = row.visibility === "public" && !row.collab_permission && !base.isSteward;
1811
+ return { ...base, permission: best, isPublicReader: isPublicReader2 };
1812
+ }
1813
+ async function resolveNestPermission(nestId, userId) {
1814
+ return (await resolveNestAccess(nestId, userId)).permission;
1713
1815
  }
1714
1816
  function permissionLevel(p) {
1715
1817
  return PERMISSION_LEVELS[p] ?? 0;
1716
1818
  }
1717
1819
  async function isPublicReader(nestId, userId) {
1718
- const db = getDb();
1719
- const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
1720
- if (!nest) return false;
1721
- if (nest.visibility !== "public") return false;
1722
- if (nest.user_id === userId) return false;
1723
- const directGrant = await db.get(
1724
- "SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
1725
- [nestId, userId]
1726
- );
1727
- if (directGrant) return false;
1728
- if (await isLicenseAdminUserId(userId)) return false;
1729
- const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
1730
- if (caller?.email && isSuperAdmin(caller.email)) return false;
1731
- const stewardGrant = await db.get(
1732
- `SELECT 1 FROM stewards s
1733
- JOIN users u ON u.id = ?
1734
- WHERE s.nest_id = ? AND s.is_active = 1
1735
- AND LOWER(s.user_email) = LOWER(u.email)
1736
- LIMIT 1`,
1737
- [userId, nestId]
1738
- );
1739
- if (stewardGrant) return false;
1740
- if ((await resolveTeamRolesForUser(nestId, userId)).length > 0) return false;
1741
- return true;
1820
+ return (await resolveNestAccess(nestId, userId)).isPublicReader;
1742
1821
  }
1743
1822
 
1744
1823
  // src/governance/tag-index-service.ts
@@ -1775,6 +1854,7 @@ async function removeNodeFromTagIndex(nestId, nodeId) {
1775
1854
  }
1776
1855
 
1777
1856
  // src/nests/service.ts
1857
+ import { rm } from "fs/promises";
1778
1858
  async function isImportedNest(nestId) {
1779
1859
  const row = await getDb().get(
1780
1860
  "SELECT is_imported FROM nests WHERE id = ?",
@@ -1954,27 +2034,16 @@ async function createNest(userId, name, description) {
1954
2034
  trackEvent("nest.create", { nestId: id, userId });
1955
2035
  return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1956
2036
  }
1957
- async function importNest(userId, name, files) {
2037
+ async function importNest(userId, name) {
1958
2038
  const nm = (name || "").trim();
1959
2039
  if (!nm) {
1960
2040
  throw new ValidationError("name is required");
1961
2041
  }
1962
- if (!Array.isArray(files)) {
1963
- throw new ValidationError("files are required");
1964
- }
1965
2042
  const db = getDb();
1966
2043
  const id = uuid2();
1967
2044
  const slug = toSlug(nm);
1968
2045
  const visibility = userId === ANON_USER_ID ? "public" : "private";
1969
- const dest = resolveNestPath(id);
1970
- mkdirSync(dest, { recursive: true });
1971
- try {
1972
- await writeImportedFiles(dest, files);
1973
- } catch (err) {
1974
- console.error("[nests] import write failed", dest, err);
1975
- discardImportDir(dest);
1976
- throw new ValidationError("Failed to import folder");
1977
- }
2046
+ mkdirSync(resolveNestPath(id), { recursive: true });
1978
2047
  await db.run(
1979
2048
  "INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)",
1980
2049
  [id, userId, nm, slug, null, visibility]
@@ -2045,10 +2114,12 @@ async function listSharedNests(userId) {
2045
2114
  LEFT JOIN stewards s
2046
2115
  ON s.nest_id = n.id AND s.is_active = 1
2047
2116
  AND LOWER(s.user_email) = LOWER(u.email)
2117
+ LEFT JOIN grants g
2118
+ ON g.nest_id = n.id AND g.user_id = ?
2048
2119
  WHERE n.user_id != ?
2049
- AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL)
2120
+ AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL OR g.id IS NOT NULL)
2050
2121
  ORDER BY n.created_at DESC`,
2051
- [userId, userId, userId]
2122
+ [userId, userId, userId, userId]
2052
2123
  );
2053
2124
  const seen = new Set(direct.map((n) => n.id));
2054
2125
  for (const n of await nestsSharedViaTeams(userId)) {
@@ -2074,10 +2145,16 @@ async function listSharedNests(userId) {
2074
2145
  );
2075
2146
  return direct;
2076
2147
  }
2077
- async function listVisibleNests(userId) {
2148
+ async function listVisibleNests(userId, opts = {}) {
2078
2149
  const db = getDb();
2079
2150
  const includeAnon = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
2080
- const params = includeAnon ? [userId, userId, userId, ANON_USER_ID] : [userId, userId, userId];
2151
+ const params = [
2152
+ userId,
2153
+ userId,
2154
+ ...opts.readable ? [userId] : [],
2155
+ userId,
2156
+ ...includeAnon ? [ANON_USER_ID] : []
2157
+ ];
2081
2158
  const rows = await db.all(
2082
2159
  `SELECT DISTINCT n.* FROM nests n
2083
2160
  LEFT JOIN nest_collaborators nc
@@ -2086,9 +2163,11 @@ async function listVisibleNests(userId) {
2086
2163
  LEFT JOIN stewards s
2087
2164
  ON s.nest_id = n.id AND s.is_active = 1
2088
2165
  AND LOWER(s.user_email) = LOWER(u.email)
2166
+ ${opts.readable ? "LEFT JOIN grants g ON g.nest_id = n.id AND g.user_id = ?" : ""}
2089
2167
  WHERE n.user_id = ?
2090
2168
  OR nc.user_id IS NOT NULL
2091
2169
  OR s.id IS NOT NULL
2170
+ ${opts.readable ? "OR g.id IS NOT NULL OR n.visibility = 'public'" : ""}
2092
2171
  ${includeAnon ? "OR n.user_id = ?" : ""}
2093
2172
  ORDER BY n.created_at DESC`,
2094
2173
  params
@@ -2138,7 +2217,7 @@ async function deleteNest(nestId) {
2138
2217
  });
2139
2218
  const path = resolveNestPath(nestId);
2140
2219
  try {
2141
- rmSync2(path, { recursive: true, force: true });
2220
+ await rm(path, { recursive: true, force: true });
2142
2221
  } catch (err) {
2143
2222
  console.warn(`[nests] failed to remove nest directory ${path}:`, err);
2144
2223
  }
@@ -2194,6 +2273,8 @@ export {
2194
2273
  getNest,
2195
2274
  deleteNest,
2196
2275
  engineCache,
2276
+ engineApi,
2277
+ opContext,
2197
2278
  buildTitleMap,
2198
2279
  folderForNode,
2199
2280
  describeNode,
@@ -2223,6 +2304,7 @@ export {
2223
2304
  canApproveWith,
2224
2305
  primaryRole,
2225
2306
  isServerAdminUserId,
2307
+ resolveNestAccess,
2226
2308
  resolveNestPermission,
2227
2309
  permissionLevel,
2228
2310
  isPublicReader
@@ -2,10 +2,10 @@ import {
2
2
  buildTitleMap,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-7BOG3S5H.js";
5
+ } from "./chunk-SNFGU5Q7.js";
6
6
  import {
7
7
  getDb
8
- } from "./chunk-BCFKLY4H.js";
8
+ } from "./chunk-JMJLNEXE.js";
9
9
 
10
10
  // src/governance/version-service.ts
11
11
  import { createHash } from "crypto";
@@ -100,6 +100,15 @@ async function getCurrentVersion(nestId, nodeId) {
100
100
  );
101
101
  return row?.v || 0;
102
102
  }
103
+ async function getTrackedNodeIds(nestId) {
104
+ const db = getDb();
105
+ const rows = await db.all(
106
+ `SELECT DISTINCT node_id FROM node_versions
107
+ WHERE nest_id = ? AND author NOT LIKE ?`,
108
+ [nestId, `${SYSTEM_AUTHOR_PREFIX}%`]
109
+ );
110
+ return new Set(rows.map((r) => r.node_id));
111
+ }
103
112
  async function getApprovedVersion(nestId, nodeId) {
104
113
  const db = getDb();
105
114
  const row = await db.get(
@@ -108,6 +117,14 @@ async function getApprovedVersion(nestId, nodeId) {
108
117
  );
109
118
  return row?.approved_version ?? null;
110
119
  }
120
+ async function getApprovedVersions(nestId) {
121
+ const db = getDb();
122
+ const rows = await db.all(
123
+ "SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
124
+ [nestId]
125
+ );
126
+ return new Map(rows.map((r) => [r.node_id, r.approved_version]));
127
+ }
111
128
  async function setApprovedVersion(nestId, nodeId, version, approvedBy) {
112
129
  const db = getDb();
113
130
  const sql = insertOrReplace(
@@ -262,7 +279,9 @@ export {
262
279
  getVersions,
263
280
  getVersion,
264
281
  getCurrentVersion,
282
+ getTrackedNodeIds,
265
283
  getApprovedVersion,
284
+ getApprovedVersions,
266
285
  setApprovedVersion,
267
286
  checkConflict,
268
287
  getNodeTags,
@@ -11,7 +11,7 @@ import {
11
11
  resolveNestPermission,
12
12
  resolveTeamRolesForUser,
13
13
  sendEmailToRecipient
14
- } from "./chunk-7BOG3S5H.js";
14
+ } from "./chunk-SNFGU5Q7.js";
15
15
  import {
16
16
  ConflictError,
17
17
  ValidationError
@@ -20,7 +20,7 @@ import {
20
20
  config,
21
21
  getDb,
22
22
  isEmailish
23
- } from "./chunk-BCFKLY4H.js";
23
+ } from "./chunk-JMJLNEXE.js";
24
24
 
25
25
  // src/governance/stewardship-service.ts
26
26
  import { v4 as uuid } from "uuid";
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-BCFKLY4H.js";
5
+ } from "./chunk-JMJLNEXE.js";
6
6
  import "./chunk-SLTQACJW.js";
7
7
  export {
8
8
  getDb,