@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.
- package/README.md +6 -2
- package/dist/{chunk-UXJSO54R.js → chunk-34UXRZXD.js} +59 -14
- package/dist/chunk-F66VCBLA.js +136 -0
- package/dist/{chunk-SNFGU5Q7.js → chunk-FUUJG6XP.js} +597 -326
- package/dist/{chunk-4YGJSUJC.js → chunk-HGN45I5P.js} +29 -21
- package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
- package/dist/chunk-OOBZG6BW.js +379 -0
- package/dist/{chunk-HWDPCPYT.js → chunk-QUZXRLUA.js} +1 -1
- package/dist/{chunk-YF4OFT2V.js → chunk-TQACHVVA.js} +35 -4
- package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
- package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
- package/dist/engine-34E3YNX7.js +14 -0
- package/dist/external-edit-service-6Q6SHEOF.js +33 -0
- package/dist/{grants-service-4NXPBYWD.js → grants-service-ME7HIVHU.js} +3 -3
- package/dist/index.js +496 -470
- package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
- package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
- package/dist/{review-service-3CCKDO5N.js → review-service-H2M5ACPQ.js} +7 -7
- package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-U4CKL7Y5.js} +8 -4
- package/dist/{version-service-EFZGGL3Y.js → version-service-JWEN5UUW.js} +6 -4
- package/dist/web3/assets/index-DONv5_Ni.css +1 -0
- package/dist/web3/assets/index-jSGrnboh.js +1065 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -3
- package/dist/chunk-XRK6SQSC.js +0 -58
- package/dist/web3/assets/index-D1299AIf.css +0 -1
- 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-
|
|
11
|
+
} from "./chunk-HYSTFMFG.js";
|
|
12
12
|
import {
|
|
13
13
|
ANON_USER_ID
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-YB3LKF7U.js";
|
|
15
15
|
import {
|
|
16
16
|
normalizeEmail
|
|
17
17
|
} from "./chunk-FRQJWGN3.js";
|
|
18
18
|
|
|
19
|
-
// src/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
27
|
-
return
|
|
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
|
-
|
|
475
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -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
|
|
657
|
-
lineH: (x) => `You've been given <strong>${escapeHtml(x.d.permission
|
|
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",
|
|
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/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
986
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
|
1080
|
+
return nodeId.split("/").pop() || nodeId;
|
|
1019
1081
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
]);
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
"
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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-
|
|
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
|
|
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,
|