@promptowl/contextnest-community 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CONFIGURATION.md +1 -1
  2. package/README.md +6 -2
  3. package/dist/{chunk-YF4OFT2V.js → chunk-3GMGLYTZ.js} +36 -5
  4. package/dist/{chunk-UXJSO54R.js → chunk-BXUVIAMP.js} +59 -14
  5. package/dist/{chunk-SNFGU5Q7.js → chunk-ENDHMQPD.js} +602 -327
  6. package/dist/chunk-F66VCBLA.js +136 -0
  7. package/dist/{chunk-4YGJSUJC.js → chunk-FBDEESNJ.js} +30 -22
  8. package/dist/{chunk-HWDPCPYT.js → chunk-GSYMJ3A4.js} +2 -2
  9. package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
  10. package/dist/chunk-TNAZJYL5.js +379 -0
  11. package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
  12. package/dist/{chunk-GUNJTORH.js → chunk-YVMSM7LS.js} +14 -1
  13. package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
  14. package/dist/engine-V4DY4PYN.js +14 -0
  15. package/dist/external-edit-service-3NK5DGLA.js +33 -0
  16. package/dist/{grants-service-4NXPBYWD.js → grants-service-CADI6LIB.js} +4 -4
  17. package/dist/index.js +580 -513
  18. package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
  19. package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
  20. package/dist/{review-service-3CCKDO5N.js → review-service-LK4M2C43.js} +8 -8
  21. package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-JAFSU5GN.js} +9 -5
  22. package/dist/{version-service-EFZGGL3Y.js → version-service-G7SFFKFO.js} +7 -5
  23. package/dist/web3/assets/index-C26nM5Kn.css +1 -0
  24. package/dist/web3/assets/index-C4oalc2K.js +1065 -0
  25. package/dist/web3/index.html +2 -2
  26. package/package.json +3 -3
  27. package/dist/chunk-XRK6SQSC.js +0 -58
  28. package/dist/web3/assets/index-D1299AIf.css +0 -1
  29. package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
@@ -3,28 +3,40 @@ import {
3
3
  ForbiddenError,
4
4
  NotFoundError,
5
5
  ValidationError
6
- } from "./chunk-GUNJTORH.js";
6
+ } from "./chunk-YVMSM7LS.js";
7
7
  import {
8
8
  config,
9
9
  getDb,
10
10
  isEmailish
11
- } from "./chunk-JMJLNEXE.js";
11
+ } from "./chunk-HYSTFMFG.js";
12
12
  import {
13
13
  ANON_USER_ID
14
- } from "./chunk-SLTQACJW.js";
14
+ } from "./chunk-YB3LKF7U.js";
15
15
  import {
16
16
  normalizeEmail
17
17
  } from "./chunk-FRQJWGN3.js";
18
18
 
19
- // src/db/sql.ts
20
- function nowExpr(db) {
21
- return db.dialect === "sqlite" ? "datetime('now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS')";
22
- }
23
- function insertOrIgnore(db, insertSql) {
24
- return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
19
+ // src/nodes/engine.ts
20
+ import {
21
+ NestStorage as NestStorage3,
22
+ GraphQueryEngine,
23
+ VersionManager,
24
+ DocumentNotFoundError
25
+ } from "@promptowl/contextnest-engine";
26
+ import { createEngineApi } from "@promptowl/contextnest-engine/api";
27
+
28
+ // src/nests/service.ts
29
+ import { mkdirSync } from "fs";
30
+ import { v4 as uuid2 } from "uuid";
31
+ import { NestStorage } from "@promptowl/contextnest-engine";
32
+
33
+ // src/shared/paths.ts
34
+ import { join } from "path";
35
+ function nestStorageRoot() {
36
+ return config.NEST_STORAGE_ROOT;
25
37
  }
26
- function insertOrReplace(db, insertSql, conflictCols, set) {
27
- return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
38
+ function resolveNestPath(nestId) {
39
+ return join(nestStorageRoot(), nestId);
28
40
  }
29
41
 
30
42
  // src/telemetry/tracker.ts
@@ -122,6 +134,17 @@ function startTelemetryLoop() {
122
134
  );
123
135
  }
124
136
 
137
+ // src/db/sql.ts
138
+ function nowExpr(db) {
139
+ return db.dialect === "sqlite" ? "datetime('now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS')";
140
+ }
141
+ function insertOrIgnore(db, insertSql) {
142
+ return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
143
+ }
144
+ function insertOrReplace(db, insertSql, conflictCols, set) {
145
+ return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
146
+ }
147
+
125
148
  // src/db/settings.ts
126
149
  var deployBaseline = null;
127
150
  function norm(v) {
@@ -460,7 +483,7 @@ async function _validateLicenseImpl(forceFresh) {
460
483
 
461
484
  // src/governance/access-service.ts
462
485
  import { readFileSync, existsSync } from "fs";
463
- import { join } from "path";
486
+ import { join as join2 } from "path";
464
487
  var accessConfig = null;
465
488
  var grantedSuperAdmins = /* @__PURE__ */ new Set();
466
489
  async function loadGrantedSuperAdmins() {
@@ -471,8 +494,8 @@ async function loadGrantedSuperAdmins() {
471
494
  }
472
495
  function loadAccessConfig() {
473
496
  const candidates = [
474
- join(config.DATA_ROOT, "access.yaml"),
475
- join(config.DATA_ROOT, "access.yml")
497
+ join2(config.DATA_ROOT, "access.yaml"),
498
+ join2(config.DATA_ROOT, "access.yml")
476
499
  ];
477
500
  for (const path of candidates) {
478
501
  if (existsSync(path)) {
@@ -566,6 +589,56 @@ function parseAccessYaml(content) {
566
589
  return result;
567
590
  }
568
591
 
592
+ // src/governance/teams-service.ts
593
+ import { v4 as uuid } from "uuid";
594
+
595
+ // src/governance/roles.ts
596
+ function collabPermToRole(permission) {
597
+ switch (permission) {
598
+ case "owner":
599
+ return "owner";
600
+ case "admin":
601
+ return "admin";
602
+ case "write":
603
+ return "editor";
604
+ case "read":
605
+ return "viewer";
606
+ default:
607
+ return null;
608
+ }
609
+ }
610
+ function teamRoleToPermission(role) {
611
+ switch (role) {
612
+ case "admin":
613
+ return "admin";
614
+ case "editor":
615
+ return "write";
616
+ case "viewer":
617
+ return "read";
618
+ default:
619
+ return null;
620
+ }
621
+ }
622
+ var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
623
+ var canViewWith = (roles) => roles.length > 0;
624
+ var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
625
+ var canApproveWith = (roles) => includesAny(roles, ["owner", "admin", "reviewer"]);
626
+ var PRECEDENCE = [
627
+ "owner",
628
+ "admin",
629
+ "editor",
630
+ "reviewer",
631
+ "viewer"
632
+ ];
633
+ function primaryRole(roles) {
634
+ for (const r of PRECEDENCE) if (roles.includes(r)) return r;
635
+ return null;
636
+ }
637
+ function permissionLabel(permission) {
638
+ const role = collabPermToRole(permission);
639
+ return role ? role[0].toUpperCase() + role.slice(1) : "Collaborator";
640
+ }
641
+
569
642
  // src/notify/email-render.ts
570
643
  function escapeHtml(s) {
571
644
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -610,6 +683,7 @@ var STATUS_META = {
610
683
  }
611
684
  };
612
685
  var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
686
+ var shareRole = (permission) => permission ? permissionLabel(permission) : "collaborator";
613
687
  var detailRow = (label, valueHtml) => `<tr>
614
688
  <td style="padding:5px 0;font-size:13px;color:#6b7280;width:90px;vertical-align:top;">${label}</td>
615
689
  <td style="padding:5px 0;font-size:13px;color:#111827;vertical-align:top;">${valueHtml}</td>
@@ -653,11 +727,11 @@ var TEMPLATES = {
653
727
  heading: (x) => `You've been added to ${x.title}`,
654
728
  detailsHeader: "Details",
655
729
  linkLabel: "Open nest",
656
- lineT: (x) => `You've been given ${x.d.permission || "collaborator"} access to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open it below to start collaborating.`,
657
- lineH: (x) => `You've been given <strong>${escapeHtml(x.d.permission || "collaborator")}</strong> access to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open it below to start collaborating.`,
730
+ lineT: (x) => `You've been given ${shareRole(x.d.permission)} access to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open it below to start collaborating.`,
731
+ lineH: (x) => `You've been given <strong>${escapeHtml(shareRole(x.d.permission))}</strong> access to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open it below to start collaborating.`,
658
732
  rows: (x) => [
659
733
  plainRow("Nest", x.d.path),
660
- ...x.d.permission ? [plainRow("Role", cap(x.d.permission))] : []
734
+ ...x.d.permission ? [plainRow("Role", shareRole(x.d.permission))] : []
661
735
  ]
662
736
  },
663
737
  invited: {
@@ -972,272 +1046,75 @@ async function sendEmailToRecipient(to, details) {
972
1046
  }
973
1047
  }
974
1048
 
975
- // src/nests/service.ts
976
- import { mkdirSync } from "fs";
977
- import { v4 as uuid2 } from "uuid";
978
- import { NestStorage as NestStorage3 } from "@promptowl/contextnest-engine";
979
-
980
- // src/shared/paths.ts
981
- import { join as join2 } from "path";
982
- function nestStorageRoot() {
983
- return config.NEST_STORAGE_ROOT;
1049
+ // src/governance/title-resolver.ts
1050
+ async function buildTitleMap(nestId) {
1051
+ try {
1052
+ const { storage } = await engineCache.get(nestId);
1053
+ const docs = await storage.discoverDocuments();
1054
+ return new Map(docs.map((d) => [d.id, d.frontmatter.title]));
1055
+ } catch {
1056
+ return /* @__PURE__ */ new Map();
1057
+ }
984
1058
  }
985
- function resolveNestPath(nestId) {
986
- return join2(nestStorageRoot(), nestId);
1059
+ function folderForNode(nodeId) {
1060
+ const segments = nodeId.split("/");
1061
+ const root = segments.indexOf("nodes");
1062
+ return segments.slice(root + 1, -1).join("/");
987
1063
  }
988
-
989
- // src/governance/teams-service.ts
990
- import { v4 as uuid } from "uuid";
991
-
992
- // src/nodes/engine.ts
993
- import {
994
- NestStorage as NestStorage2,
995
- GraphQueryEngine,
996
- VersionManager,
997
- DocumentNotFoundError
998
- } from "@promptowl/contextnest-engine";
999
- import { createEngineApi } from "@promptowl/contextnest-engine/api";
1000
-
1001
- // src/nodes/flat-storage.ts
1002
- import { readdir } from "fs/promises";
1003
- import { join as join3, relative, sep } from "path";
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";
1064
+ async function describeNode(nestId, nodeId) {
1065
+ const title = await titleForNode(nestId, nodeId);
1066
+ const folder = folderForNode(nodeId);
1067
+ return folder ? `${title} (in ${folder})` : title;
1009
1068
  }
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)));
1069
+ async function titleForNode(nestId, nodeId) {
1070
+ const titles = await buildTitleMap(nestId);
1071
+ return titles.get(nodeId) || titleFallback(titles, nodeId);
1072
+ }
1073
+ function titleFallback(titles, nodeId) {
1074
+ const at = nodeId.indexOf("nodes/");
1075
+ if (at > 0) {
1076
+ const normalized = nodeId.slice(at);
1077
+ const hit = titles.get(normalized);
1078
+ if (hit) return hit;
1017
1079
  }
1018
- return out;
1080
+ return nodeId.split("/").pop() || nodeId;
1019
1081
  }
1020
-
1021
- // src/nodes/flat-storage.ts
1022
- var META_FILES = /* @__PURE__ */ new Set([
1023
- "INDEX.md",
1024
- "CONTEXT.md",
1025
- "CLAUDE.md",
1026
- "GEMINI.md",
1027
- "AGENTS.md",
1028
- "QWEN.md"
1029
- ]);
1030
- var FlatNestStorage = class extends NestStorage {
1031
- async discoverDocuments() {
1032
- const root = this.root;
1033
- let entries;
1034
- try {
1035
- entries = await readdir(root, { recursive: true, withFileTypes: true });
1036
- } catch (err) {
1037
- console.error("[FlatNestStorage] cannot read folder", root, err);
1038
- return [];
1039
- }
1040
- const ids = [];
1041
- for (const e of entries) {
1042
- if (!e.isFile() || !e.name.endsWith(".md")) continue;
1043
- if (META_FILES.has(e.name)) continue;
1044
- const dir = e.parentPath ?? e.path ?? root;
1045
- const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
1046
- if (rel.split("/").some(isSkippedSegment)) continue;
1047
- ids.push(rel.replace(/\.md$/, ""));
1048
- }
1049
- ids.sort();
1050
- const read = await mapBatched(ids, async (id) => {
1051
- try {
1052
- return await this.readDocument(id);
1053
- } catch (err) {
1054
- console.error("[FlatNestStorage] skipped unreadable doc", id, err);
1055
- return null;
1056
- }
1057
- });
1058
- return read.filter((n) => n !== null);
1082
+ function prettyFolderPath(path) {
1083
+ return path.replace(/^nodes\//, "").split("/").filter(Boolean).map(
1084
+ (seg) => seg.split(/[-_]/).map((s) => s ? s[0].toUpperCase() + s.slice(1) : s).join(" ")
1085
+ ).join(" / ");
1086
+ }
1087
+ async function nestName(nestId) {
1088
+ try {
1089
+ const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
1090
+ nestId
1091
+ ]);
1092
+ return row?.name || nestId;
1093
+ } catch {
1094
+ return nestId;
1059
1095
  }
1060
- };
1061
-
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
- }
1148
- var NestEngineCache = class {
1149
- cache = /* @__PURE__ */ new Map();
1150
- async get(nestId) {
1151
- let engine = this.cache.get(nestId);
1152
- if (!engine) {
1153
- const nestPath = resolveNestPath(nestId);
1154
- const { storage, invalidate } = withDiscoveryCache(
1155
- await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath)
1156
- );
1157
- const query = new GraphQueryEngine(storage);
1158
- const versions = new VersionManager(storage);
1159
- engine = { storage, query, versions, invalidateDiscovery: invalidate };
1160
- this.cache.set(nestId, engine);
1161
- }
1162
- return engine;
1163
- }
1164
- evict(nestId) {
1165
- this.cache.delete(nestId);
1166
- }
1167
- };
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
- }
1174
-
1175
- // src/governance/title-resolver.ts
1176
- async function buildTitleMap(nestId) {
1177
- try {
1178
- const { storage } = await engineCache.get(nestId);
1179
- const docs = await storage.discoverDocuments();
1180
- return new Map(docs.map((d) => [d.id, d.frontmatter.title]));
1181
- } catch {
1182
- return /* @__PURE__ */ new Map();
1183
- }
1184
- }
1185
- function folderForNode(nodeId) {
1186
- const segments = nodeId.split("/");
1187
- const root = segments.indexOf("nodes");
1188
- return segments.slice(root + 1, -1).join("/");
1189
- }
1190
- async function describeNode(nestId, nodeId) {
1191
- const title = await titleForNode(nestId, nodeId);
1192
- const folder = folderForNode(nodeId);
1193
- return folder ? `${title} (in ${folder})` : title;
1194
- }
1195
- async function titleForNode(nestId, nodeId) {
1196
- const titles = await buildTitleMap(nestId);
1197
- return titles.get(nodeId) || titleFallback(titles, nodeId);
1198
- }
1199
- function titleFallback(titles, nodeId) {
1200
- const at = nodeId.indexOf("nodes/");
1201
- if (at > 0) {
1202
- const normalized = nodeId.slice(at);
1203
- const hit = titles.get(normalized);
1204
- if (hit) return hit;
1205
- }
1206
- return nodeId.split("/").pop() || nodeId;
1207
- }
1208
- function prettyFolderPath(path) {
1209
- return path.replace(/^nodes\//, "").split("/").filter(Boolean).map(
1210
- (seg) => seg.split(/[-_]/).map((s) => s ? s[0].toUpperCase() + s.slice(1) : s).join(" ")
1211
- ).join(" / ");
1212
- }
1213
- async function nestName(nestId) {
1214
- try {
1215
- const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
1216
- nestId
1217
- ]);
1218
- return row?.name || nestId;
1219
- } catch {
1220
- return nestId;
1221
- }
1222
- }
1223
- function docLink(nestId, nodeId, baseUrl) {
1224
- const base = baseUrl || config.PUBLIC_BASE_URL;
1225
- if (!base) return "";
1226
- const q = new URLSearchParams({ nest: nestId });
1227
- if (nodeId) q.set("doc", nodeId);
1228
- return `${base.replace(/\/$/, "")}/?${q.toString()}`;
1229
- }
1230
- async function buildDocContext(nestId, nodeId, baseUrl) {
1231
- const titles = await buildTitleMap(nestId);
1232
- const docTitle = titles.get(nodeId) || nodeId;
1233
- const folder = folderForNode(nodeId);
1234
- const name = await nestName(nestId);
1235
- return {
1236
- docTitle,
1237
- // Slack one-liner label ("Title (in Folder)"), same as describeNode().
1238
- label: folder ? `${docTitle} (in ${folder})` : docTitle,
1239
- path: folder ? `${name} / ${folder}` : name,
1240
- link: docLink(nestId, nodeId, baseUrl)
1096
+ }
1097
+ function docLink(nestId, nodeId, baseUrl) {
1098
+ const base = baseUrl || config.PUBLIC_BASE_URL;
1099
+ if (!base) return "";
1100
+ const q = new URLSearchParams({ nest: nestId });
1101
+ if (nodeId) q.set("doc", nodeId);
1102
+ return `${base.replace(/\/$/, "")}/?${q.toString()}`;
1103
+ }
1104
+ function resolveNodeUrl(node, opts) {
1105
+ return docLink(opts.nestId, node.id, opts.baseUrl) || void 0;
1106
+ }
1107
+ async function buildDocContext(nestId, nodeId, baseUrl) {
1108
+ const titles = await buildTitleMap(nestId);
1109
+ const docTitle = titles.get(nodeId) || nodeId;
1110
+ const folder = folderForNode(nodeId);
1111
+ const name = await nestName(nestId);
1112
+ return {
1113
+ docTitle,
1114
+ // Slack one-liner label ("Title (in Folder)"), same as describeNode().
1115
+ label: folder ? `${docTitle} (in ${folder})` : docTitle,
1116
+ path: folder ? `${name} / ${folder}` : name,
1117
+ link: docLink(nestId, nodeId, baseUrl)
1241
1118
  };
1242
1119
  }
1243
1120
 
@@ -1438,7 +1315,7 @@ async function resolveMemberIdentity(params) {
1438
1315
  email: normalizeEmail(existing.email ?? requested)
1439
1316
  };
1440
1317
  }
1441
- const { hashPassword } = await import("./keys-73STFJJB.js");
1318
+ const { hashPassword } = await import("./keys-CTOGAG3W.js");
1442
1319
  const userId = uuid();
1443
1320
  await db.run(
1444
1321
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
@@ -1674,49 +1551,6 @@ async function resolveTeamRolesFromShared(refs, userId) {
1674
1551
  return [...roles];
1675
1552
  }
1676
1553
 
1677
- // src/governance/roles.ts
1678
- function collabPermToRole(permission) {
1679
- switch (permission) {
1680
- case "owner":
1681
- return "owner";
1682
- case "admin":
1683
- return "admin";
1684
- case "write":
1685
- return "editor";
1686
- case "read":
1687
- return "viewer";
1688
- default:
1689
- return null;
1690
- }
1691
- }
1692
- function teamRoleToPermission(role) {
1693
- switch (role) {
1694
- case "admin":
1695
- return "admin";
1696
- case "editor":
1697
- return "write";
1698
- case "viewer":
1699
- return "read";
1700
- default:
1701
- return null;
1702
- }
1703
- }
1704
- var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
1705
- var canViewWith = (roles) => roles.length > 0;
1706
- var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
1707
- var canApproveWith = (roles) => includesAny(roles, ["owner", "admin", "reviewer"]);
1708
- var PRECEDENCE = [
1709
- "owner",
1710
- "admin",
1711
- "editor",
1712
- "reviewer",
1713
- "viewer"
1714
- ];
1715
- function primaryRole(roles) {
1716
- for (const r of PRECEDENCE) if (roles.includes(r)) return r;
1717
- return null;
1718
- }
1719
-
1720
1554
  // src/shared/access.ts
1721
1555
  async function isServerAdminUserId(userId) {
1722
1556
  if (await isLicenseAdminUserId(userId)) return true;
@@ -2029,7 +1863,7 @@ async function createNest(userId, name, description) {
2029
1863
  );
2030
1864
  const path = resolveNestPath(id);
2031
1865
  mkdirSync(path, { recursive: true });
2032
- const storage = new NestStorage3(path);
1866
+ const storage = new NestStorage(path);
2033
1867
  await storage.init(trimmed);
2034
1868
  trackEvent("nest.create", { nestId: id, userId });
2035
1869
  return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
@@ -2224,6 +2058,435 @@ async function deleteNest(nestId) {
2224
2058
  trackEvent("nest.delete", { nestId });
2225
2059
  }
2226
2060
 
2061
+ // src/nodes/flat-storage.ts
2062
+ import { readdir } from "fs/promises";
2063
+ import { join as join3, relative, sep } from "path";
2064
+ import { NestStorage as NestStorage2 } from "@promptowl/contextnest-engine";
2065
+
2066
+ // src/fs/vault-import.ts
2067
+ function isSkippedSegment(segment) {
2068
+ return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
2069
+ }
2070
+
2071
+ // src/shared/batch.ts
2072
+ var IO_CONCURRENCY = 32;
2073
+ async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
2074
+ const out = [];
2075
+ for (let i = 0; i < items.length; i += concurrency) {
2076
+ out.push(...await Promise.all(items.slice(i, i + concurrency).map(fn)));
2077
+ }
2078
+ return out;
2079
+ }
2080
+
2081
+ // src/nodes/flat-storage.ts
2082
+ var META_FILES = /* @__PURE__ */ new Set([
2083
+ "INDEX.md",
2084
+ "CONTEXT.md",
2085
+ "CLAUDE.md",
2086
+ "GEMINI.md",
2087
+ "AGENTS.md",
2088
+ "QWEN.md"
2089
+ ]);
2090
+ var FlatNestStorage = class extends NestStorage2 {
2091
+ async discoverDocuments() {
2092
+ const root = this.root;
2093
+ let entries;
2094
+ try {
2095
+ entries = await readdir(root, { recursive: true, withFileTypes: true });
2096
+ } catch (err) {
2097
+ console.error("[FlatNestStorage] cannot read folder", root, err);
2098
+ return [];
2099
+ }
2100
+ const ids = [];
2101
+ for (const e of entries) {
2102
+ if (!e.isFile() || !e.name.endsWith(".md")) continue;
2103
+ if (META_FILES.has(e.name)) continue;
2104
+ const dir = e.parentPath ?? e.path ?? root;
2105
+ const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
2106
+ if (rel.split("/").some(isSkippedSegment)) continue;
2107
+ ids.push(rel.replace(/\.md$/, ""));
2108
+ }
2109
+ ids.sort();
2110
+ const read = await mapBatched(ids, async (id) => {
2111
+ try {
2112
+ return await this.readDocument(id);
2113
+ } catch (err) {
2114
+ console.error("[FlatNestStorage] skipped unreadable doc", id, err);
2115
+ return null;
2116
+ }
2117
+ });
2118
+ return read.filter((n) => n !== null);
2119
+ }
2120
+ };
2121
+
2122
+ // src/shared/node-id.ts
2123
+ function assertSafeNodeId(rawId) {
2124
+ let id = rawId;
2125
+ try {
2126
+ id = decodeURIComponent(rawId);
2127
+ } catch {
2128
+ }
2129
+ if (!id || id.length > 512) {
2130
+ throw new ValidationError("invalid node id");
2131
+ }
2132
+ if (id.includes("\0") || id.includes("\\")) {
2133
+ throw new ValidationError("invalid node id");
2134
+ }
2135
+ if (id.startsWith("/")) {
2136
+ throw new ValidationError("invalid node id (absolute path)");
2137
+ }
2138
+ for (const seg of id.split("/")) {
2139
+ if (seg === "" || seg === "." || seg === "..") {
2140
+ throw new ValidationError("invalid node id (path traversal)");
2141
+ }
2142
+ }
2143
+ return id;
2144
+ }
2145
+ function folderPathOf(path) {
2146
+ if (path === "nodes") return "";
2147
+ return path.startsWith("nodes/") ? path.slice(6) : path;
2148
+ }
2149
+ function folderOfId(id) {
2150
+ const rel = folderPathOf(id);
2151
+ const slash = rel.lastIndexOf("/");
2152
+ return slash > 0 ? rel.slice(0, slash) : "";
2153
+ }
2154
+
2155
+ // src/nodes/node-index.ts
2156
+ var INSERT_COLS = "(nest_id, node_id, folder, title, type, status, updated_at, frontmatter_json)";
2157
+ var INSERT_VALUES = "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
2158
+ var UPSERT_SET = [
2159
+ "folder = excluded.folder",
2160
+ "title = excluded.title",
2161
+ "type = excluded.type",
2162
+ "status = excluded.status",
2163
+ "updated_at = excluded.updated_at",
2164
+ "frontmatter_json = excluded.frontmatter_json"
2165
+ ].join(", ");
2166
+ function rowValues(node) {
2167
+ const fm = node.frontmatter;
2168
+ return [
2169
+ node.id,
2170
+ folderOfId(node.id),
2171
+ fm.title === void 0 || fm.title === null ? "" : String(fm.title),
2172
+ fm.type || "document",
2173
+ fm.status || "draft",
2174
+ fm.updated_at ?? null,
2175
+ JSON.stringify(fm ?? {})
2176
+ ];
2177
+ }
2178
+ function toNode(row) {
2179
+ return {
2180
+ id: row.node_id,
2181
+ filePath: "",
2182
+ rawContent: "",
2183
+ // A row written by this process is always valid JSON; a hand-edited or
2184
+ // half-written one must not take the whole listing down with it.
2185
+ frontmatter: safeParse(row.frontmatter_json),
2186
+ body: ""
2187
+ };
2188
+ }
2189
+ function safeParse(json) {
2190
+ try {
2191
+ return JSON.parse(json);
2192
+ } catch {
2193
+ return {};
2194
+ }
2195
+ }
2196
+ async function syncIndexedNode(nestId, nodeId, node) {
2197
+ writeGeneration.set(nestId, (writeGeneration.get(nestId) ?? 0) + 1);
2198
+ try {
2199
+ const db = getDb();
2200
+ if (!node) {
2201
+ await db.run(
2202
+ "DELETE FROM node_index WHERE nest_id = ? AND node_id = ?",
2203
+ [nestId, nodeId]
2204
+ );
2205
+ return;
2206
+ }
2207
+ await db.run(
2208
+ insertOrReplace(
2209
+ db,
2210
+ `INSERT INTO node_index ${INSERT_COLS} ${INSERT_VALUES}`,
2211
+ ["nest_id", "node_id"],
2212
+ UPSERT_SET
2213
+ ),
2214
+ [nestId, ...rowValues(node)]
2215
+ );
2216
+ } catch (err) {
2217
+ console.error("[node-index] sync failed, marking stale", nestId, nodeId, err);
2218
+ await markIndexStale(nestId);
2219
+ }
2220
+ }
2221
+ async function markIndexStale(nestId) {
2222
+ try {
2223
+ await getDb().run(
2224
+ "UPDATE nests SET index_synced_at = NULL WHERE id = ?",
2225
+ [nestId]
2226
+ );
2227
+ } catch (err) {
2228
+ console.error("[node-index] could not mark stale", nestId, err);
2229
+ }
2230
+ }
2231
+ var rebuilds = /* @__PURE__ */ new Map();
2232
+ var writeGeneration = /* @__PURE__ */ new Map();
2233
+ async function ensureNodeIndex(nestId) {
2234
+ const row = await getDb().get(
2235
+ "SELECT index_synced_at FROM nests WHERE id = ?",
2236
+ [nestId]
2237
+ );
2238
+ if (!row || row.index_synced_at) return;
2239
+ const inflight = rebuilds.get(nestId);
2240
+ if (inflight) return inflight;
2241
+ const run = rebuildNodeIndex(nestId).then(() => void 0).catch((err) => {
2242
+ console.error("[node-index] rebuild failed", nestId, err);
2243
+ }).finally(() => {
2244
+ if (rebuilds.get(nestId) === run) rebuilds.delete(nestId);
2245
+ });
2246
+ rebuilds.set(nestId, run);
2247
+ return run;
2248
+ }
2249
+ async function rebuildNodeIndex(nestId) {
2250
+ const { engineCache: engineCache2 } = await import("./engine-V4DY4PYN.js");
2251
+ const { documentsWithSuggestions } = await import("./external-edit-service-3NK5DGLA.js");
2252
+ const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
2253
+ dropDiscoveryCache();
2254
+ const startedAt = writeGeneration.get(nestId) ?? 0;
2255
+ const documents = await storage.discoverDocuments({ includeRetired: true });
2256
+ const staged = await documentsWithSuggestions(nestId);
2257
+ const db = getDb();
2258
+ const insertSql = insertOrReplace(
2259
+ db,
2260
+ `INSERT INTO node_index ${INSERT_COLS} ${INSERT_VALUES}`,
2261
+ ["nest_id", "node_id"],
2262
+ UPSERT_SET
2263
+ );
2264
+ const stagedSql = insertOrIgnore(
2265
+ db,
2266
+ "INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
2267
+ );
2268
+ await db.transaction(async (tx) => {
2269
+ await tx.run("DELETE FROM node_index WHERE nest_id = ?", [nestId]);
2270
+ for (const doc of documents) {
2271
+ await tx.run(insertSql, [nestId, ...rowValues(doc)]);
2272
+ }
2273
+ await tx.run("DELETE FROM node_suggestions WHERE nest_id = ?", [nestId]);
2274
+ for (const id of staged) {
2275
+ await tx.run(stagedSql, [nestId, id]);
2276
+ }
2277
+ if ((writeGeneration.get(nestId) ?? 0) !== startedAt) return;
2278
+ await tx.run(
2279
+ `UPDATE nests SET index_synced_at = ${nowExpr(db)} WHERE id = ?`,
2280
+ [nestId]
2281
+ );
2282
+ });
2283
+ return documents.length;
2284
+ }
2285
+ async function setSuggestionFlag(nestId, nodeId, present) {
2286
+ try {
2287
+ const db = getDb();
2288
+ if (!present) {
2289
+ await db.run(
2290
+ "DELETE FROM node_suggestions WHERE nest_id = ? AND node_id = ?",
2291
+ [nestId, nodeId]
2292
+ );
2293
+ return;
2294
+ }
2295
+ await db.run(
2296
+ insertOrIgnore(
2297
+ db,
2298
+ "INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
2299
+ ),
2300
+ [nestId, nodeId]
2301
+ );
2302
+ } catch (err) {
2303
+ console.error("[node-index] suggestion flag failed", nestId, nodeId, err);
2304
+ }
2305
+ }
2306
+ async function nodesWithSuggestions(nestId) {
2307
+ const rows = await getDb().all(
2308
+ "SELECT node_id FROM node_suggestions WHERE nest_id = ?",
2309
+ [nestId]
2310
+ );
2311
+ return new Set(rows.map((r) => r.node_id));
2312
+ }
2313
+ async function indexedNodeIds(nestId) {
2314
+ const rows = await getDb().all(
2315
+ "SELECT node_id FROM node_index WHERE nest_id = ?",
2316
+ [nestId]
2317
+ );
2318
+ return new Set(rows.map((r) => r.node_id));
2319
+ }
2320
+ async function readIndexedNodes(nestId, filters = {}) {
2321
+ const where = ["nest_id = ?"];
2322
+ const params = [nestId];
2323
+ if (filters.folder !== void 0) {
2324
+ where.push("folder = ?");
2325
+ params.push(filters.folder);
2326
+ }
2327
+ const rows = await getDb().all(
2328
+ `SELECT node_id, frontmatter_json FROM node_index
2329
+ WHERE ${where.join(" AND ")}
2330
+ ORDER BY node_id`,
2331
+ params
2332
+ );
2333
+ return rows.map(toNode);
2334
+ }
2335
+ async function indexedFolderCounts(nestId) {
2336
+ const rows = await getDb().all(
2337
+ `SELECT folder, COUNT(*) AS count FROM node_index
2338
+ WHERE nest_id = ? AND folder <> ''
2339
+ GROUP BY folder`,
2340
+ [nestId]
2341
+ );
2342
+ return rows.map((r) => ({ path: r.folder, count: Number(r.count) }));
2343
+ }
2344
+
2345
+ // src/nodes/engine.ts
2346
+ var DISCOVERY_TTL_MS = 3e4;
2347
+ var isLive = (node) => node.frontmatter.status !== "rejected";
2348
+ var DERIVED_VAULT_FILES = /* @__PURE__ */ new Set([
2349
+ "INDEX.md",
2350
+ "CONTEXT.md",
2351
+ "CLAUDE.md",
2352
+ "GEMINI.md",
2353
+ "AGENTS.md",
2354
+ "README.md"
2355
+ ]);
2356
+ function withDiscoveryCache(nestId, storage) {
2357
+ const entries = /* @__PURE__ */ new Map();
2358
+ let inflight = null;
2359
+ const discover = storage.discoverDocuments.bind(storage);
2360
+ const readDoc = storage.readDocument.bind(storage);
2361
+ const filtersRetired = !(storage instanceof FlatNestStorage);
2362
+ let generation = 0;
2363
+ const crawl = (options) => {
2364
+ if (inflight) return inflight;
2365
+ const startedAt = generation;
2366
+ const run = discover({ ...options, includeRetired: true }).then((crawled) => {
2367
+ const sets = {
2368
+ all: crawled,
2369
+ live: filtersRetired ? crawled.filter(isLive) : crawled
2370
+ };
2371
+ if (generation === startedAt) {
2372
+ const ts = Date.now();
2373
+ entries.set("all", { docs: sets.all, ts });
2374
+ entries.set("live", { docs: sets.live, ts });
2375
+ }
2376
+ return sets;
2377
+ }).finally(() => {
2378
+ if (inflight === run) inflight = null;
2379
+ });
2380
+ inflight = run;
2381
+ return run;
2382
+ };
2383
+ storage.discoverDocuments = async (options = {}) => {
2384
+ const key = options.includeRetired || options.includeSuperseded ? "all" : "live";
2385
+ if (options.folder !== void 0) {
2386
+ const crawled = await discover({ ...options, includeRetired: true });
2387
+ return key === "all" || !filtersRetired ? crawled : crawled.filter(isLive);
2388
+ }
2389
+ const hit = entries.get(key);
2390
+ if (hit) {
2391
+ if (Date.now() - hit.ts >= DISCOVERY_TTL_MS) {
2392
+ void crawl(options).catch((err) => {
2393
+ console.error("[discovery] background refresh failed", err);
2394
+ });
2395
+ }
2396
+ return hit.docs;
2397
+ }
2398
+ return (await crawl(options))[key];
2399
+ };
2400
+ const invalidate = () => {
2401
+ generation++;
2402
+ entries.clear();
2403
+ inflight = null;
2404
+ };
2405
+ const desync = async () => {
2406
+ invalidate();
2407
+ await markIndexStale(nestId);
2408
+ };
2409
+ const patch = async (id) => {
2410
+ generation++;
2411
+ inflight = null;
2412
+ let node = null;
2413
+ try {
2414
+ node = await readDoc(id);
2415
+ } catch (err) {
2416
+ if (!(err instanceof DocumentNotFoundError)) {
2417
+ await desync();
2418
+ return;
2419
+ }
2420
+ node = null;
2421
+ }
2422
+ await syncIndexedNode(nestId, id, node);
2423
+ for (const [key, entry] of entries) {
2424
+ const docs = entry.docs.filter((d) => d.id !== id);
2425
+ if (node && (key === "all" || isLive(node))) {
2426
+ const at = docs.findIndex((d) => d.id > id);
2427
+ docs.splice(at === -1 ? docs.length : at, 0, node);
2428
+ }
2429
+ entry.docs = docs;
2430
+ }
2431
+ };
2432
+ const wrap = (name, after) => {
2433
+ const write = storage[name].bind(storage);
2434
+ storage[name] = async (...args) => {
2435
+ try {
2436
+ return await write(...args);
2437
+ } finally {
2438
+ try {
2439
+ await after(args);
2440
+ } catch {
2441
+ await desync();
2442
+ }
2443
+ }
2444
+ };
2445
+ };
2446
+ wrap("writeDocument", (args) => patch(String(args[0])));
2447
+ wrap("deleteDocument", (args) => patch(String(args[0])));
2448
+ wrap("init", () => desync());
2449
+ wrap("writeVaultFile", (args) => {
2450
+ const rel = String(args[0] ?? "");
2451
+ const base = rel.split(/[/\\]/).pop() || "";
2452
+ if (rel.endsWith(".md") && !DERIVED_VAULT_FILES.has(base)) return desync();
2453
+ });
2454
+ return { storage, invalidate, desync: () => void desync() };
2455
+ }
2456
+ var NestEngineCache = class {
2457
+ cache = /* @__PURE__ */ new Map();
2458
+ async get(nestId) {
2459
+ let engine = this.cache.get(nestId);
2460
+ if (!engine) {
2461
+ const nestPath = resolveNestPath(nestId);
2462
+ const { storage, invalidate, desync } = withDiscoveryCache(
2463
+ nestId,
2464
+ await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage3(nestPath)
2465
+ );
2466
+ const query = new GraphQueryEngine(storage);
2467
+ const versions = new VersionManager(storage);
2468
+ engine = {
2469
+ storage,
2470
+ query,
2471
+ versions,
2472
+ invalidateDiscovery: desync,
2473
+ dropDiscoveryCache: invalidate
2474
+ };
2475
+ this.cache.set(nestId, engine);
2476
+ }
2477
+ return engine;
2478
+ }
2479
+ evict(nestId) {
2480
+ this.cache.delete(nestId);
2481
+ }
2482
+ };
2483
+ var engineCache = new NestEngineCache();
2484
+ var engineApi = createEngineApi();
2485
+ async function opContext(nestId, actor, onProgress) {
2486
+ const { storage, query, versions } = await engineCache.get(nestId);
2487
+ return { storage, query, versions, actor, onProgress };
2488
+ }
2489
+
2227
2490
  export {
2228
2491
  nowExpr,
2229
2492
  insertOrIgnore,
@@ -2245,6 +2508,12 @@ export {
2245
2508
  getAccessConfig,
2246
2509
  isSuperAdmin,
2247
2510
  isConfigSuperAdmin,
2511
+ collabPermToRole,
2512
+ canViewWith,
2513
+ canEditWith,
2514
+ canApproveWith,
2515
+ primaryRole,
2516
+ permissionLabel,
2248
2517
  verifySmtp,
2249
2518
  notifyEmailForNest,
2250
2519
  sendEmailToRecipient,
@@ -2272,6 +2541,16 @@ export {
2272
2541
  listPublicNests,
2273
2542
  getNest,
2274
2543
  deleteNest,
2544
+ assertSafeNodeId,
2545
+ folderOfId,
2546
+ markIndexStale,
2547
+ ensureNodeIndex,
2548
+ rebuildNodeIndex,
2549
+ setSuggestionFlag,
2550
+ nodesWithSuggestions,
2551
+ indexedNodeIds,
2552
+ readIndexedNodes,
2553
+ indexedFolderCounts,
2275
2554
  engineCache,
2276
2555
  engineApi,
2277
2556
  opContext,
@@ -2282,6 +2561,7 @@ export {
2282
2561
  prettyFolderPath,
2283
2562
  nestName,
2284
2563
  docLink,
2564
+ resolveNodeUrl,
2285
2565
  buildDocContext,
2286
2566
  createTeam,
2287
2567
  findTeamByExternalId,
@@ -2298,11 +2578,6 @@ export {
2298
2578
  shareTeamWithNest,
2299
2579
  unshareTeamFromNest,
2300
2580
  resolveTeamRolesForUser,
2301
- collabPermToRole,
2302
- canViewWith,
2303
- canEditWith,
2304
- canApproveWith,
2305
- primaryRole,
2306
2581
  isServerAdminUserId,
2307
2582
  resolveNestAccess,
2308
2583
  resolveNestPermission,