@promptowl/contextnest-community 1.17.0 → 1.18.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 (27) hide show
  1. package/README.md +6 -2
  2. package/dist/{chunk-UXJSO54R.js → chunk-34UXRZXD.js} +59 -14
  3. package/dist/chunk-F66VCBLA.js +136 -0
  4. package/dist/{chunk-SNFGU5Q7.js → chunk-FUUJG6XP.js} +597 -326
  5. package/dist/{chunk-4YGJSUJC.js → chunk-HGN45I5P.js} +29 -21
  6. package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
  7. package/dist/chunk-OOBZG6BW.js +379 -0
  8. package/dist/{chunk-HWDPCPYT.js → chunk-QUZXRLUA.js} +1 -1
  9. package/dist/{chunk-YF4OFT2V.js → chunk-TQACHVVA.js} +35 -4
  10. package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
  11. package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
  12. package/dist/engine-34E3YNX7.js +14 -0
  13. package/dist/external-edit-service-6Q6SHEOF.js +33 -0
  14. package/dist/{grants-service-4NXPBYWD.js → grants-service-ME7HIVHU.js} +3 -3
  15. package/dist/index.js +496 -470
  16. package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
  17. package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
  18. package/dist/{review-service-3CCKDO5N.js → review-service-H2M5ACPQ.js} +7 -7
  19. package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-U4CKL7Y5.js} +8 -4
  20. package/dist/{version-service-EFZGGL3Y.js → version-service-JWEN5UUW.js} +6 -4
  21. package/dist/web3/assets/index-DONv5_Ni.css +1 -0
  22. package/dist/web3/assets/index-jSGrnboh.js +1065 -0
  23. package/dist/web3/index.html +2 -2
  24. package/package.json +3 -3
  25. package/dist/chunk-XRK6SQSC.js +0 -58
  26. package/dist/web3/assets/index-D1299AIf.css +0 -1
  27. package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
@@ -8,23 +8,35 @@ 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,72 @@ 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
+ async function buildDocContext(nestId, nodeId, baseUrl) {
1105
+ const titles = await buildTitleMap(nestId);
1106
+ const docTitle = titles.get(nodeId) || nodeId;
1107
+ const folder = folderForNode(nodeId);
1108
+ const name = await nestName(nestId);
1109
+ return {
1110
+ docTitle,
1111
+ // Slack one-liner label ("Title (in Folder)"), same as describeNode().
1112
+ label: folder ? `${docTitle} (in ${folder})` : docTitle,
1113
+ path: folder ? `${name} / ${folder}` : name,
1114
+ link: docLink(nestId, nodeId, baseUrl)
1241
1115
  };
1242
1116
  }
1243
1117
 
@@ -1438,7 +1312,7 @@ async function resolveMemberIdentity(params) {
1438
1312
  email: normalizeEmail(existing.email ?? requested)
1439
1313
  };
1440
1314
  }
1441
- const { hashPassword } = await import("./keys-73STFJJB.js");
1315
+ const { hashPassword } = await import("./keys-CTOGAG3W.js");
1442
1316
  const userId = uuid();
1443
1317
  await db.run(
1444
1318
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
@@ -1674,49 +1548,6 @@ async function resolveTeamRolesFromShared(refs, userId) {
1674
1548
  return [...roles];
1675
1549
  }
1676
1550
 
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
1551
  // src/shared/access.ts
1721
1552
  async function isServerAdminUserId(userId) {
1722
1553
  if (await isLicenseAdminUserId(userId)) return true;
@@ -2029,7 +1860,7 @@ async function createNest(userId, name, description) {
2029
1860
  );
2030
1861
  const path = resolveNestPath(id);
2031
1862
  mkdirSync(path, { recursive: true });
2032
- const storage = new NestStorage3(path);
1863
+ const storage = new NestStorage(path);
2033
1864
  await storage.init(trimmed);
2034
1865
  trackEvent("nest.create", { nestId: id, userId });
2035
1866
  return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
@@ -2224,6 +2055,435 @@ async function deleteNest(nestId) {
2224
2055
  trackEvent("nest.delete", { nestId });
2225
2056
  }
2226
2057
 
2058
+ // src/nodes/flat-storage.ts
2059
+ import { readdir } from "fs/promises";
2060
+ import { join as join3, relative, sep } from "path";
2061
+ import { NestStorage as NestStorage2 } from "@promptowl/contextnest-engine";
2062
+
2063
+ // src/fs/vault-import.ts
2064
+ function isSkippedSegment(segment) {
2065
+ return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
2066
+ }
2067
+
2068
+ // src/shared/batch.ts
2069
+ var IO_CONCURRENCY = 32;
2070
+ async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
2071
+ const out = [];
2072
+ for (let i = 0; i < items.length; i += concurrency) {
2073
+ out.push(...await Promise.all(items.slice(i, i + concurrency).map(fn)));
2074
+ }
2075
+ return out;
2076
+ }
2077
+
2078
+ // src/nodes/flat-storage.ts
2079
+ var META_FILES = /* @__PURE__ */ new Set([
2080
+ "INDEX.md",
2081
+ "CONTEXT.md",
2082
+ "CLAUDE.md",
2083
+ "GEMINI.md",
2084
+ "AGENTS.md",
2085
+ "QWEN.md"
2086
+ ]);
2087
+ var FlatNestStorage = class extends NestStorage2 {
2088
+ async discoverDocuments() {
2089
+ const root = this.root;
2090
+ let entries;
2091
+ try {
2092
+ entries = await readdir(root, { recursive: true, withFileTypes: true });
2093
+ } catch (err) {
2094
+ console.error("[FlatNestStorage] cannot read folder", root, err);
2095
+ return [];
2096
+ }
2097
+ const ids = [];
2098
+ for (const e of entries) {
2099
+ if (!e.isFile() || !e.name.endsWith(".md")) continue;
2100
+ if (META_FILES.has(e.name)) continue;
2101
+ const dir = e.parentPath ?? e.path ?? root;
2102
+ const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
2103
+ if (rel.split("/").some(isSkippedSegment)) continue;
2104
+ ids.push(rel.replace(/\.md$/, ""));
2105
+ }
2106
+ ids.sort();
2107
+ const read = await mapBatched(ids, async (id) => {
2108
+ try {
2109
+ return await this.readDocument(id);
2110
+ } catch (err) {
2111
+ console.error("[FlatNestStorage] skipped unreadable doc", id, err);
2112
+ return null;
2113
+ }
2114
+ });
2115
+ return read.filter((n) => n !== null);
2116
+ }
2117
+ };
2118
+
2119
+ // src/shared/node-id.ts
2120
+ function assertSafeNodeId(rawId) {
2121
+ let id = rawId;
2122
+ try {
2123
+ id = decodeURIComponent(rawId);
2124
+ } catch {
2125
+ }
2126
+ if (!id || id.length > 512) {
2127
+ throw new ValidationError("invalid node id");
2128
+ }
2129
+ if (id.includes("\0") || id.includes("\\")) {
2130
+ throw new ValidationError("invalid node id");
2131
+ }
2132
+ if (id.startsWith("/")) {
2133
+ throw new ValidationError("invalid node id (absolute path)");
2134
+ }
2135
+ for (const seg of id.split("/")) {
2136
+ if (seg === "" || seg === "." || seg === "..") {
2137
+ throw new ValidationError("invalid node id (path traversal)");
2138
+ }
2139
+ }
2140
+ return id;
2141
+ }
2142
+ function folderPathOf(path) {
2143
+ if (path === "nodes") return "";
2144
+ return path.startsWith("nodes/") ? path.slice(6) : path;
2145
+ }
2146
+ function folderOfId(id) {
2147
+ const rel = folderPathOf(id);
2148
+ const slash = rel.lastIndexOf("/");
2149
+ return slash > 0 ? rel.slice(0, slash) : "";
2150
+ }
2151
+
2152
+ // src/nodes/node-index.ts
2153
+ var INSERT_COLS = "(nest_id, node_id, folder, title, type, status, updated_at, frontmatter_json)";
2154
+ var INSERT_VALUES = "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
2155
+ var UPSERT_SET = [
2156
+ "folder = excluded.folder",
2157
+ "title = excluded.title",
2158
+ "type = excluded.type",
2159
+ "status = excluded.status",
2160
+ "updated_at = excluded.updated_at",
2161
+ "frontmatter_json = excluded.frontmatter_json"
2162
+ ].join(", ");
2163
+ function rowValues(node) {
2164
+ const fm = node.frontmatter;
2165
+ return [
2166
+ node.id,
2167
+ folderOfId(node.id),
2168
+ fm.title === void 0 || fm.title === null ? "" : String(fm.title),
2169
+ fm.type || "document",
2170
+ fm.status || "draft",
2171
+ fm.updated_at ?? null,
2172
+ JSON.stringify(fm ?? {})
2173
+ ];
2174
+ }
2175
+ function toNode(row) {
2176
+ return {
2177
+ id: row.node_id,
2178
+ filePath: "",
2179
+ rawContent: "",
2180
+ // A row written by this process is always valid JSON; a hand-edited or
2181
+ // half-written one must not take the whole listing down with it.
2182
+ frontmatter: safeParse(row.frontmatter_json),
2183
+ body: ""
2184
+ };
2185
+ }
2186
+ function safeParse(json) {
2187
+ try {
2188
+ return JSON.parse(json);
2189
+ } catch {
2190
+ return {};
2191
+ }
2192
+ }
2193
+ async function syncIndexedNode(nestId, nodeId, node) {
2194
+ writeGeneration.set(nestId, (writeGeneration.get(nestId) ?? 0) + 1);
2195
+ try {
2196
+ const db = getDb();
2197
+ if (!node) {
2198
+ await db.run(
2199
+ "DELETE FROM node_index WHERE nest_id = ? AND node_id = ?",
2200
+ [nestId, nodeId]
2201
+ );
2202
+ return;
2203
+ }
2204
+ await db.run(
2205
+ insertOrReplace(
2206
+ db,
2207
+ `INSERT INTO node_index ${INSERT_COLS} ${INSERT_VALUES}`,
2208
+ ["nest_id", "node_id"],
2209
+ UPSERT_SET
2210
+ ),
2211
+ [nestId, ...rowValues(node)]
2212
+ );
2213
+ } catch (err) {
2214
+ console.error("[node-index] sync failed, marking stale", nestId, nodeId, err);
2215
+ await markIndexStale(nestId);
2216
+ }
2217
+ }
2218
+ async function markIndexStale(nestId) {
2219
+ try {
2220
+ await getDb().run(
2221
+ "UPDATE nests SET index_synced_at = NULL WHERE id = ?",
2222
+ [nestId]
2223
+ );
2224
+ } catch (err) {
2225
+ console.error("[node-index] could not mark stale", nestId, err);
2226
+ }
2227
+ }
2228
+ var rebuilds = /* @__PURE__ */ new Map();
2229
+ var writeGeneration = /* @__PURE__ */ new Map();
2230
+ async function ensureNodeIndex(nestId) {
2231
+ const row = await getDb().get(
2232
+ "SELECT index_synced_at FROM nests WHERE id = ?",
2233
+ [nestId]
2234
+ );
2235
+ if (!row || row.index_synced_at) return;
2236
+ const inflight = rebuilds.get(nestId);
2237
+ if (inflight) return inflight;
2238
+ const run = rebuildNodeIndex(nestId).then(() => void 0).catch((err) => {
2239
+ console.error("[node-index] rebuild failed", nestId, err);
2240
+ }).finally(() => {
2241
+ if (rebuilds.get(nestId) === run) rebuilds.delete(nestId);
2242
+ });
2243
+ rebuilds.set(nestId, run);
2244
+ return run;
2245
+ }
2246
+ async function rebuildNodeIndex(nestId) {
2247
+ const { engineCache: engineCache2 } = await import("./engine-34E3YNX7.js");
2248
+ const { documentsWithSuggestions } = await import("./external-edit-service-6Q6SHEOF.js");
2249
+ const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
2250
+ dropDiscoveryCache();
2251
+ const startedAt = writeGeneration.get(nestId) ?? 0;
2252
+ const documents = await storage.discoverDocuments({ includeRetired: true });
2253
+ const staged = await documentsWithSuggestions(nestId);
2254
+ const db = getDb();
2255
+ const insertSql = insertOrReplace(
2256
+ db,
2257
+ `INSERT INTO node_index ${INSERT_COLS} ${INSERT_VALUES}`,
2258
+ ["nest_id", "node_id"],
2259
+ UPSERT_SET
2260
+ );
2261
+ const stagedSql = insertOrIgnore(
2262
+ db,
2263
+ "INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
2264
+ );
2265
+ await db.transaction(async (tx) => {
2266
+ await tx.run("DELETE FROM node_index WHERE nest_id = ?", [nestId]);
2267
+ for (const doc of documents) {
2268
+ await tx.run(insertSql, [nestId, ...rowValues(doc)]);
2269
+ }
2270
+ await tx.run("DELETE FROM node_suggestions WHERE nest_id = ?", [nestId]);
2271
+ for (const id of staged) {
2272
+ await tx.run(stagedSql, [nestId, id]);
2273
+ }
2274
+ if ((writeGeneration.get(nestId) ?? 0) !== startedAt) return;
2275
+ await tx.run(
2276
+ `UPDATE nests SET index_synced_at = ${nowExpr(db)} WHERE id = ?`,
2277
+ [nestId]
2278
+ );
2279
+ });
2280
+ return documents.length;
2281
+ }
2282
+ async function setSuggestionFlag(nestId, nodeId, present) {
2283
+ try {
2284
+ const db = getDb();
2285
+ if (!present) {
2286
+ await db.run(
2287
+ "DELETE FROM node_suggestions WHERE nest_id = ? AND node_id = ?",
2288
+ [nestId, nodeId]
2289
+ );
2290
+ return;
2291
+ }
2292
+ await db.run(
2293
+ insertOrIgnore(
2294
+ db,
2295
+ "INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
2296
+ ),
2297
+ [nestId, nodeId]
2298
+ );
2299
+ } catch (err) {
2300
+ console.error("[node-index] suggestion flag failed", nestId, nodeId, err);
2301
+ }
2302
+ }
2303
+ async function nodesWithSuggestions(nestId) {
2304
+ const rows = await getDb().all(
2305
+ "SELECT node_id FROM node_suggestions WHERE nest_id = ?",
2306
+ [nestId]
2307
+ );
2308
+ return new Set(rows.map((r) => r.node_id));
2309
+ }
2310
+ async function indexedNodeIds(nestId) {
2311
+ const rows = await getDb().all(
2312
+ "SELECT node_id FROM node_index WHERE nest_id = ?",
2313
+ [nestId]
2314
+ );
2315
+ return new Set(rows.map((r) => r.node_id));
2316
+ }
2317
+ async function readIndexedNodes(nestId, filters = {}) {
2318
+ const where = ["nest_id = ?"];
2319
+ const params = [nestId];
2320
+ if (filters.folder !== void 0) {
2321
+ where.push("folder = ?");
2322
+ params.push(filters.folder);
2323
+ }
2324
+ const rows = await getDb().all(
2325
+ `SELECT node_id, frontmatter_json FROM node_index
2326
+ WHERE ${where.join(" AND ")}
2327
+ ORDER BY node_id`,
2328
+ params
2329
+ );
2330
+ return rows.map(toNode);
2331
+ }
2332
+ async function indexedFolderCounts(nestId) {
2333
+ const rows = await getDb().all(
2334
+ `SELECT folder, COUNT(*) AS count FROM node_index
2335
+ WHERE nest_id = ? AND folder <> ''
2336
+ GROUP BY folder`,
2337
+ [nestId]
2338
+ );
2339
+ return rows.map((r) => ({ path: r.folder, count: Number(r.count) }));
2340
+ }
2341
+
2342
+ // src/nodes/engine.ts
2343
+ var DISCOVERY_TTL_MS = 3e4;
2344
+ var isLive = (node) => node.frontmatter.status !== "rejected";
2345
+ var DERIVED_VAULT_FILES = /* @__PURE__ */ new Set([
2346
+ "INDEX.md",
2347
+ "CONTEXT.md",
2348
+ "CLAUDE.md",
2349
+ "GEMINI.md",
2350
+ "AGENTS.md",
2351
+ "README.md"
2352
+ ]);
2353
+ function withDiscoveryCache(nestId, storage) {
2354
+ const entries = /* @__PURE__ */ new Map();
2355
+ let inflight = null;
2356
+ const discover = storage.discoverDocuments.bind(storage);
2357
+ const readDoc = storage.readDocument.bind(storage);
2358
+ const filtersRetired = !(storage instanceof FlatNestStorage);
2359
+ let generation = 0;
2360
+ const crawl = (options) => {
2361
+ if (inflight) return inflight;
2362
+ const startedAt = generation;
2363
+ const run = discover({ ...options, includeRetired: true }).then((crawled) => {
2364
+ const sets = {
2365
+ all: crawled,
2366
+ live: filtersRetired ? crawled.filter(isLive) : crawled
2367
+ };
2368
+ if (generation === startedAt) {
2369
+ const ts = Date.now();
2370
+ entries.set("all", { docs: sets.all, ts });
2371
+ entries.set("live", { docs: sets.live, ts });
2372
+ }
2373
+ return sets;
2374
+ }).finally(() => {
2375
+ if (inflight === run) inflight = null;
2376
+ });
2377
+ inflight = run;
2378
+ return run;
2379
+ };
2380
+ storage.discoverDocuments = async (options = {}) => {
2381
+ const key = options.includeRetired || options.includeSuperseded ? "all" : "live";
2382
+ if (options.folder !== void 0) {
2383
+ const crawled = await discover({ ...options, includeRetired: true });
2384
+ return key === "all" || !filtersRetired ? crawled : crawled.filter(isLive);
2385
+ }
2386
+ const hit = entries.get(key);
2387
+ if (hit) {
2388
+ if (Date.now() - hit.ts >= DISCOVERY_TTL_MS) {
2389
+ void crawl(options).catch((err) => {
2390
+ console.error("[discovery] background refresh failed", err);
2391
+ });
2392
+ }
2393
+ return hit.docs;
2394
+ }
2395
+ return (await crawl(options))[key];
2396
+ };
2397
+ const invalidate = () => {
2398
+ generation++;
2399
+ entries.clear();
2400
+ inflight = null;
2401
+ };
2402
+ const desync = async () => {
2403
+ invalidate();
2404
+ await markIndexStale(nestId);
2405
+ };
2406
+ const patch = async (id) => {
2407
+ generation++;
2408
+ inflight = null;
2409
+ let node = null;
2410
+ try {
2411
+ node = await readDoc(id);
2412
+ } catch (err) {
2413
+ if (!(err instanceof DocumentNotFoundError)) {
2414
+ await desync();
2415
+ return;
2416
+ }
2417
+ node = null;
2418
+ }
2419
+ await syncIndexedNode(nestId, id, node);
2420
+ for (const [key, entry] of entries) {
2421
+ const docs = entry.docs.filter((d) => d.id !== id);
2422
+ if (node && (key === "all" || isLive(node))) {
2423
+ const at = docs.findIndex((d) => d.id > id);
2424
+ docs.splice(at === -1 ? docs.length : at, 0, node);
2425
+ }
2426
+ entry.docs = docs;
2427
+ }
2428
+ };
2429
+ const wrap = (name, after) => {
2430
+ const write = storage[name].bind(storage);
2431
+ storage[name] = async (...args) => {
2432
+ try {
2433
+ return await write(...args);
2434
+ } finally {
2435
+ try {
2436
+ await after(args);
2437
+ } catch {
2438
+ await desync();
2439
+ }
2440
+ }
2441
+ };
2442
+ };
2443
+ wrap("writeDocument", (args) => patch(String(args[0])));
2444
+ wrap("deleteDocument", (args) => patch(String(args[0])));
2445
+ wrap("init", () => desync());
2446
+ wrap("writeVaultFile", (args) => {
2447
+ const rel = String(args[0] ?? "");
2448
+ const base = rel.split(/[/\\]/).pop() || "";
2449
+ if (rel.endsWith(".md") && !DERIVED_VAULT_FILES.has(base)) return desync();
2450
+ });
2451
+ return { storage, invalidate, desync: () => void desync() };
2452
+ }
2453
+ var NestEngineCache = class {
2454
+ cache = /* @__PURE__ */ new Map();
2455
+ async get(nestId) {
2456
+ let engine = this.cache.get(nestId);
2457
+ if (!engine) {
2458
+ const nestPath = resolveNestPath(nestId);
2459
+ const { storage, invalidate, desync } = withDiscoveryCache(
2460
+ nestId,
2461
+ await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage3(nestPath)
2462
+ );
2463
+ const query = new GraphQueryEngine(storage);
2464
+ const versions = new VersionManager(storage);
2465
+ engine = {
2466
+ storage,
2467
+ query,
2468
+ versions,
2469
+ invalidateDiscovery: desync,
2470
+ dropDiscoveryCache: invalidate
2471
+ };
2472
+ this.cache.set(nestId, engine);
2473
+ }
2474
+ return engine;
2475
+ }
2476
+ evict(nestId) {
2477
+ this.cache.delete(nestId);
2478
+ }
2479
+ };
2480
+ var engineCache = new NestEngineCache();
2481
+ var engineApi = createEngineApi();
2482
+ async function opContext(nestId, actor, onProgress) {
2483
+ const { storage, query, versions } = await engineCache.get(nestId);
2484
+ return { storage, query, versions, actor, onProgress };
2485
+ }
2486
+
2227
2487
  export {
2228
2488
  nowExpr,
2229
2489
  insertOrIgnore,
@@ -2245,6 +2505,12 @@ export {
2245
2505
  getAccessConfig,
2246
2506
  isSuperAdmin,
2247
2507
  isConfigSuperAdmin,
2508
+ collabPermToRole,
2509
+ canViewWith,
2510
+ canEditWith,
2511
+ canApproveWith,
2512
+ primaryRole,
2513
+ permissionLabel,
2248
2514
  verifySmtp,
2249
2515
  notifyEmailForNest,
2250
2516
  sendEmailToRecipient,
@@ -2272,6 +2538,16 @@ export {
2272
2538
  listPublicNests,
2273
2539
  getNest,
2274
2540
  deleteNest,
2541
+ assertSafeNodeId,
2542
+ folderOfId,
2543
+ markIndexStale,
2544
+ ensureNodeIndex,
2545
+ rebuildNodeIndex,
2546
+ setSuggestionFlag,
2547
+ nodesWithSuggestions,
2548
+ indexedNodeIds,
2549
+ readIndexedNodes,
2550
+ indexedFolderCounts,
2275
2551
  engineCache,
2276
2552
  engineApi,
2277
2553
  opContext,
@@ -2298,11 +2574,6 @@ export {
2298
2574
  shareTeamWithNest,
2299
2575
  unshareTeamFromNest,
2300
2576
  resolveTeamRolesForUser,
2301
- collabPermToRole,
2302
- canViewWith,
2303
- canEditWith,
2304
- canApproveWith,
2305
- primaryRole,
2306
2577
  isServerAdminUserId,
2307
2578
  resolveNestAccess,
2308
2579
  resolveNestPermission,