@promptowl/contextnest-community 1.16.1 → 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/CONFIGURATION.md +415 -415
- package/LICENSE.md +142 -142
- package/README.md +168 -169
- package/dist/{chunk-D6HICFVW.js → chunk-34UXRZXD.js} +59 -14
- package/dist/chunk-F66VCBLA.js +136 -0
- package/dist/{chunk-L7UCMGWZ.js → chunk-FUUJG6XP.js} +641 -285
- package/dist/{chunk-QJVRZT6M.js → chunk-HGN45I5P.js} +59 -21
- package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
- package/dist/chunk-OOBZG6BW.js +379 -0
- package/dist/{chunk-OTGZQZKK.js → chunk-QUZXRLUA.js} +11 -10
- package/dist/{chunk-CLKZZZS2.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-I535QVTW.js → grants-service-ME7HIVHU.js} +5 -5
- package/dist/index.js +700 -526
- 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-V2GR6VM3.js → review-service-H2M5ACPQ.js} +11 -9
- package/dist/{stewardship-service-LTREGVSJ.js → stewardship-service-U4CKL7Y5.js} +8 -4
- package/dist/{version-service-V6K5SZ3P.js → version-service-JWEN5UUW.js} +6 -4
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-DONv5_Ni.css +1 -0
- package/dist/web3/assets/index-jSGrnboh.js +1065 -0
- package/dist/web3/index.html +14 -14
- package/package.json +165 -165
- package/dist/chunk-XRK6SQSC.js +0 -58
- package/dist/web3/assets/index-DnQARUL3.css +0 -1
- package/dist/web3/assets/index-_m7WyozM.js +0 -1051
|
@@ -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,156 +1046,6 @@ 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;
|
|
984
|
-
}
|
|
985
|
-
function resolveNestPath(nestId) {
|
|
986
|
-
return join2(nestStorageRoot(), nestId);
|
|
987
|
-
}
|
|
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
|
-
} from "@promptowl/contextnest-engine";
|
|
998
|
-
import { createEngineApi } from "@promptowl/contextnest-engine/api";
|
|
999
|
-
|
|
1000
|
-
// src/nodes/flat-storage.ts
|
|
1001
|
-
import { readdir } from "fs/promises";
|
|
1002
|
-
import { join as join3, relative, sep } from "path";
|
|
1003
|
-
import { NestStorage } from "@promptowl/contextnest-engine";
|
|
1004
|
-
|
|
1005
|
-
// src/fs/vault-import.ts
|
|
1006
|
-
function isSkippedSegment(segment) {
|
|
1007
|
-
return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// src/shared/batch.ts
|
|
1011
|
-
var IO_CONCURRENCY = 32;
|
|
1012
|
-
async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
|
|
1013
|
-
const out = [];
|
|
1014
|
-
for (let i = 0; i < items.length; i += concurrency) {
|
|
1015
|
-
out.push(...await Promise.all(items.slice(i, i + concurrency).map(fn)));
|
|
1016
|
-
}
|
|
1017
|
-
return out;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// src/nodes/flat-storage.ts
|
|
1021
|
-
var META_FILES = /* @__PURE__ */ new Set([
|
|
1022
|
-
"INDEX.md",
|
|
1023
|
-
"CONTEXT.md",
|
|
1024
|
-
"CLAUDE.md",
|
|
1025
|
-
"GEMINI.md",
|
|
1026
|
-
"AGENTS.md",
|
|
1027
|
-
"QWEN.md"
|
|
1028
|
-
]);
|
|
1029
|
-
var FlatNestStorage = class extends NestStorage {
|
|
1030
|
-
async discoverDocuments() {
|
|
1031
|
-
const root = this.root;
|
|
1032
|
-
let entries;
|
|
1033
|
-
try {
|
|
1034
|
-
entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
1035
|
-
} catch (err) {
|
|
1036
|
-
console.error("[FlatNestStorage] cannot read folder", root, err);
|
|
1037
|
-
return [];
|
|
1038
|
-
}
|
|
1039
|
-
const ids = [];
|
|
1040
|
-
for (const e of entries) {
|
|
1041
|
-
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
1042
|
-
if (META_FILES.has(e.name)) continue;
|
|
1043
|
-
const dir = e.parentPath ?? e.path ?? root;
|
|
1044
|
-
const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
|
|
1045
|
-
if (rel.split("/").some(isSkippedSegment)) continue;
|
|
1046
|
-
ids.push(rel.replace(/\.md$/, ""));
|
|
1047
|
-
}
|
|
1048
|
-
ids.sort();
|
|
1049
|
-
const read = await mapBatched(ids, async (id) => {
|
|
1050
|
-
try {
|
|
1051
|
-
return await this.readDocument(id);
|
|
1052
|
-
} catch (err) {
|
|
1053
|
-
console.error("[FlatNestStorage] skipped unreadable doc", id, err);
|
|
1054
|
-
return null;
|
|
1055
|
-
}
|
|
1056
|
-
});
|
|
1057
|
-
return read.filter((n) => n !== null);
|
|
1058
|
-
}
|
|
1059
|
-
};
|
|
1060
|
-
|
|
1061
|
-
// src/nodes/engine.ts
|
|
1062
|
-
var DISCOVERY_TTL_MS = 3e4;
|
|
1063
|
-
var DOCUMENT_WRITERS = [
|
|
1064
|
-
"writeDocument",
|
|
1065
|
-
"deleteDocument",
|
|
1066
|
-
"writeVaultFile",
|
|
1067
|
-
"init"
|
|
1068
|
-
];
|
|
1069
|
-
function withDiscoveryCache(storage) {
|
|
1070
|
-
const entries = /* @__PURE__ */ new Map();
|
|
1071
|
-
const discover = storage.discoverDocuments.bind(storage);
|
|
1072
|
-
let generation = 0;
|
|
1073
|
-
storage.discoverDocuments = async (options = {}) => {
|
|
1074
|
-
const key = options.includeRetired || options.includeSuperseded ? "all" : "live";
|
|
1075
|
-
const hit = entries.get(key);
|
|
1076
|
-
if (hit && Date.now() - hit.ts < DISCOVERY_TTL_MS) return hit.docs;
|
|
1077
|
-
const startedAt = generation;
|
|
1078
|
-
const docs = await discover(options);
|
|
1079
|
-
if (generation === startedAt) entries.set(key, { docs, ts: Date.now() });
|
|
1080
|
-
return docs;
|
|
1081
|
-
};
|
|
1082
|
-
const invalidate = () => {
|
|
1083
|
-
generation++;
|
|
1084
|
-
entries.clear();
|
|
1085
|
-
};
|
|
1086
|
-
for (const name of DOCUMENT_WRITERS) {
|
|
1087
|
-
const write = storage[name].bind(storage);
|
|
1088
|
-
storage[name] = async (...args) => {
|
|
1089
|
-
try {
|
|
1090
|
-
return await write(...args);
|
|
1091
|
-
} finally {
|
|
1092
|
-
invalidate();
|
|
1093
|
-
}
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
|
-
return { storage, invalidate };
|
|
1097
|
-
}
|
|
1098
|
-
var NestEngineCache = class {
|
|
1099
|
-
cache = /* @__PURE__ */ new Map();
|
|
1100
|
-
async get(nestId) {
|
|
1101
|
-
let engine = this.cache.get(nestId);
|
|
1102
|
-
if (!engine) {
|
|
1103
|
-
const nestPath = resolveNestPath(nestId);
|
|
1104
|
-
const { storage, invalidate } = withDiscoveryCache(
|
|
1105
|
-
await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath)
|
|
1106
|
-
);
|
|
1107
|
-
const query = new GraphQueryEngine(storage);
|
|
1108
|
-
const versions = new VersionManager(storage);
|
|
1109
|
-
engine = { storage, query, versions, invalidateDiscovery: invalidate };
|
|
1110
|
-
this.cache.set(nestId, engine);
|
|
1111
|
-
}
|
|
1112
|
-
return engine;
|
|
1113
|
-
}
|
|
1114
|
-
evict(nestId) {
|
|
1115
|
-
this.cache.delete(nestId);
|
|
1116
|
-
}
|
|
1117
|
-
};
|
|
1118
|
-
var engineCache = new NestEngineCache();
|
|
1119
|
-
var engineApi = createEngineApi();
|
|
1120
|
-
async function opContext(nestId, actor, onProgress) {
|
|
1121
|
-
const { storage, query, versions } = await engineCache.get(nestId);
|
|
1122
|
-
return { storage, query, versions, actor, onProgress };
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
1049
|
// src/governance/title-resolver.ts
|
|
1126
1050
|
async function buildTitleMap(nestId) {
|
|
1127
1051
|
try {
|
|
@@ -1388,7 +1312,7 @@ async function resolveMemberIdentity(params) {
|
|
|
1388
1312
|
email: normalizeEmail(existing.email ?? requested)
|
|
1389
1313
|
};
|
|
1390
1314
|
}
|
|
1391
|
-
const { hashPassword } = await import("./keys-
|
|
1315
|
+
const { hashPassword } = await import("./keys-CTOGAG3W.js");
|
|
1392
1316
|
const userId = uuid();
|
|
1393
1317
|
await db.run(
|
|
1394
1318
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -1606,63 +1530,24 @@ async function removeTeamRefs(tx, teamId) {
|
|
|
1606
1530
|
}
|
|
1607
1531
|
async function resolveTeamRolesForUser(nestId, userId) {
|
|
1608
1532
|
const refs = await listNestTeams(nestId).catch(() => []);
|
|
1533
|
+
return resolveTeamRolesFromShared(refs, userId);
|
|
1534
|
+
}
|
|
1535
|
+
async function resolveTeamRolesFromShared(refs, userId) {
|
|
1609
1536
|
if (refs.length === 0) return [];
|
|
1610
|
-
const
|
|
1537
|
+
const ids = [...new Set(refs.map((r) => r.teamId).filter(Boolean))];
|
|
1538
|
+
if (ids.length === 0) return [];
|
|
1539
|
+
const rows = await getDb().all(
|
|
1540
|
+
`SELECT members FROM teams WHERE id IN (${ids.map(() => "?").join(",")})`,
|
|
1541
|
+
ids
|
|
1542
|
+
);
|
|
1611
1543
|
const roles = /* @__PURE__ */ new Set();
|
|
1612
|
-
for (const
|
|
1613
|
-
const row = await db.get("SELECT members FROM teams WHERE id = ?", [
|
|
1614
|
-
ref.teamId
|
|
1615
|
-
]);
|
|
1616
|
-
if (!row) continue;
|
|
1544
|
+
for (const row of rows) {
|
|
1617
1545
|
const member = parseMembers(row.members).find((m) => m.userId === userId);
|
|
1618
1546
|
if (member) roles.add(member.role);
|
|
1619
1547
|
}
|
|
1620
1548
|
return [...roles];
|
|
1621
1549
|
}
|
|
1622
1550
|
|
|
1623
|
-
// src/governance/roles.ts
|
|
1624
|
-
function collabPermToRole(permission) {
|
|
1625
|
-
switch (permission) {
|
|
1626
|
-
case "owner":
|
|
1627
|
-
return "owner";
|
|
1628
|
-
case "admin":
|
|
1629
|
-
return "admin";
|
|
1630
|
-
case "write":
|
|
1631
|
-
return "editor";
|
|
1632
|
-
case "read":
|
|
1633
|
-
return "viewer";
|
|
1634
|
-
default:
|
|
1635
|
-
return null;
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
function teamRoleToPermission(role) {
|
|
1639
|
-
switch (role) {
|
|
1640
|
-
case "admin":
|
|
1641
|
-
return "admin";
|
|
1642
|
-
case "editor":
|
|
1643
|
-
return "write";
|
|
1644
|
-
case "viewer":
|
|
1645
|
-
return "read";
|
|
1646
|
-
default:
|
|
1647
|
-
return null;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
|
|
1651
|
-
var canViewWith = (roles) => roles.length > 0;
|
|
1652
|
-
var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
|
|
1653
|
-
var canApproveWith = (roles) => includesAny(roles, ["owner", "admin", "reviewer"]);
|
|
1654
|
-
var PRECEDENCE = [
|
|
1655
|
-
"owner",
|
|
1656
|
-
"admin",
|
|
1657
|
-
"editor",
|
|
1658
|
-
"reviewer",
|
|
1659
|
-
"viewer"
|
|
1660
|
-
];
|
|
1661
|
-
function primaryRole(roles) {
|
|
1662
|
-
for (const r of PRECEDENCE) if (roles.includes(r)) return r;
|
|
1663
|
-
return null;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
1551
|
// src/shared/access.ts
|
|
1667
1552
|
async function isServerAdminUserId(userId) {
|
|
1668
1553
|
if (await isLicenseAdminUserId(userId)) return true;
|
|
@@ -1679,71 +1564,91 @@ var PERMISSION_LEVELS = {
|
|
|
1679
1564
|
admin: 3,
|
|
1680
1565
|
owner: 4
|
|
1681
1566
|
};
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
for (const role of await resolveTeamRolesForUser(nestId, userId)) {
|
|
1704
|
-
best = higher(best, teamRoleToPermission(role));
|
|
1705
|
-
}
|
|
1706
|
-
if (best === "admin") return best;
|
|
1707
|
-
const stewardGrant = await db.get(
|
|
1708
|
-
`SELECT 1 FROM stewards s
|
|
1709
|
-
JOIN users u ON u.id = ?
|
|
1710
|
-
WHERE s.nest_id = ? AND s.is_active = 1
|
|
1711
|
-
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1712
|
-
LIMIT 1`,
|
|
1713
|
-
[userId, nestId]
|
|
1567
|
+
var higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
|
|
1568
|
+
async function resolveNestAccess(nestId, userId) {
|
|
1569
|
+
const row = await getDb().get(
|
|
1570
|
+
`SELECT n.user_id AS owner_id,
|
|
1571
|
+
n.visibility AS visibility,
|
|
1572
|
+
n.shared_teams AS shared_teams,
|
|
1573
|
+
u.email AS caller_email,
|
|
1574
|
+
nc.permission AS collab_permission,
|
|
1575
|
+
(SELECT 1 FROM stewards s
|
|
1576
|
+
WHERE s.nest_id = n.id AND s.is_active = 1
|
|
1577
|
+
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1578
|
+
LIMIT 1) AS steward_hit,
|
|
1579
|
+
(SELECT 1 FROM grants g
|
|
1580
|
+
WHERE g.nest_id = n.id AND g.user_id = ?
|
|
1581
|
+
LIMIT 1) AS grant_hit
|
|
1582
|
+
FROM nests n
|
|
1583
|
+
LEFT JOIN users u ON u.id = ?
|
|
1584
|
+
LEFT JOIN nest_collaborators nc
|
|
1585
|
+
ON nc.nest_id = n.id AND nc.user_id = ?
|
|
1586
|
+
WHERE n.id = ?`,
|
|
1587
|
+
[userId, userId, userId, nestId]
|
|
1714
1588
|
);
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1589
|
+
const absent = {
|
|
1590
|
+
exists: false,
|
|
1591
|
+
ownerId: null,
|
|
1592
|
+
visibility: null,
|
|
1593
|
+
callerEmail: null,
|
|
1594
|
+
collaboratorPermission: null,
|
|
1595
|
+
isSteward: false,
|
|
1596
|
+
hasGrant: false,
|
|
1597
|
+
permission: "none",
|
|
1598
|
+
isPublicReader: false
|
|
1599
|
+
};
|
|
1600
|
+
if (!row) return absent;
|
|
1601
|
+
const base = {
|
|
1602
|
+
exists: true,
|
|
1603
|
+
ownerId: row.owner_id,
|
|
1604
|
+
visibility: row.visibility,
|
|
1605
|
+
callerEmail: row.caller_email ?? null,
|
|
1606
|
+
collaboratorPermission: row.collab_permission ?? null,
|
|
1607
|
+
// Postgres returns integer 1, SQLite returns 1 — both truthy, neither a
|
|
1608
|
+
// boolean, so normalize rather than leaking the driver's shape.
|
|
1609
|
+
isSteward: !!row.steward_hit,
|
|
1610
|
+
hasGrant: !!row.grant_hit
|
|
1611
|
+
};
|
|
1612
|
+
if (row.owner_id === userId) {
|
|
1613
|
+
return { ...base, permission: "owner", isPublicReader: false };
|
|
1614
|
+
}
|
|
1615
|
+
const email = row.caller_email ?? null;
|
|
1616
|
+
const licenseAdmin = isLicenseAdminEmail(email);
|
|
1617
|
+
if (row.owner_id === ANON_USER_ID && licenseAdmin) {
|
|
1618
|
+
return { ...base, permission: "owner", isPublicReader: false };
|
|
1619
|
+
}
|
|
1620
|
+
if (licenseAdmin || email && isSuperAdmin(email)) {
|
|
1621
|
+
return { ...base, permission: "admin", isPublicReader: false };
|
|
1622
|
+
}
|
|
1623
|
+
let best = higher("none", row.collab_permission);
|
|
1624
|
+
if (best !== "admin") {
|
|
1625
|
+
const teamRoles = await resolveTeamRolesFromShared(
|
|
1626
|
+
parseSharedTeams(row.shared_teams),
|
|
1627
|
+
userId
|
|
1628
|
+
);
|
|
1629
|
+
for (const role of teamRoles) {
|
|
1630
|
+
best = higher(best, teamRoleToPermission(role));
|
|
1631
|
+
}
|
|
1632
|
+
if (teamRoles.length > 0) {
|
|
1633
|
+
return { ...base, permission: best, isPublicReader: false };
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (best === "admin") {
|
|
1637
|
+
return { ...base, permission: best, isPublicReader: false };
|
|
1638
|
+
}
|
|
1639
|
+
if (base.isSteward) best = higher(best, "read");
|
|
1640
|
+
if (row.visibility === "public") best = higher(best, "read");
|
|
1641
|
+
const isPublicReader2 = row.visibility === "public" && !row.collab_permission && !base.isSteward;
|
|
1642
|
+
return { ...base, permission: best, isPublicReader: isPublicReader2 };
|
|
1643
|
+
}
|
|
1644
|
+
async function resolveNestPermission(nestId, userId) {
|
|
1645
|
+
return (await resolveNestAccess(nestId, userId)).permission;
|
|
1718
1646
|
}
|
|
1719
1647
|
function permissionLevel(p) {
|
|
1720
1648
|
return PERMISSION_LEVELS[p] ?? 0;
|
|
1721
1649
|
}
|
|
1722
1650
|
async function isPublicReader(nestId, userId) {
|
|
1723
|
-
|
|
1724
|
-
const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
|
|
1725
|
-
if (!nest) return false;
|
|
1726
|
-
if (nest.visibility !== "public") return false;
|
|
1727
|
-
if (nest.user_id === userId) return false;
|
|
1728
|
-
const directGrant = await db.get(
|
|
1729
|
-
"SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
1730
|
-
[nestId, userId]
|
|
1731
|
-
);
|
|
1732
|
-
if (directGrant) return false;
|
|
1733
|
-
if (await isLicenseAdminUserId(userId)) return false;
|
|
1734
|
-
const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
1735
|
-
if (caller?.email && isSuperAdmin(caller.email)) return false;
|
|
1736
|
-
const stewardGrant = await db.get(
|
|
1737
|
-
`SELECT 1 FROM stewards s
|
|
1738
|
-
JOIN users u ON u.id = ?
|
|
1739
|
-
WHERE s.nest_id = ? AND s.is_active = 1
|
|
1740
|
-
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1741
|
-
LIMIT 1`,
|
|
1742
|
-
[userId, nestId]
|
|
1743
|
-
);
|
|
1744
|
-
if (stewardGrant) return false;
|
|
1745
|
-
if ((await resolveTeamRolesForUser(nestId, userId)).length > 0) return false;
|
|
1746
|
-
return true;
|
|
1651
|
+
return (await resolveNestAccess(nestId, userId)).isPublicReader;
|
|
1747
1652
|
}
|
|
1748
1653
|
|
|
1749
1654
|
// src/governance/tag-index-service.ts
|
|
@@ -1955,7 +1860,7 @@ async function createNest(userId, name, description) {
|
|
|
1955
1860
|
);
|
|
1956
1861
|
const path = resolveNestPath(id);
|
|
1957
1862
|
mkdirSync(path, { recursive: true });
|
|
1958
|
-
const storage = new
|
|
1863
|
+
const storage = new NestStorage(path);
|
|
1959
1864
|
await storage.init(trimmed);
|
|
1960
1865
|
trackEvent("nest.create", { nestId: id, userId });
|
|
1961
1866
|
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
@@ -2040,10 +1945,12 @@ async function listSharedNests(userId) {
|
|
|
2040
1945
|
LEFT JOIN stewards s
|
|
2041
1946
|
ON s.nest_id = n.id AND s.is_active = 1
|
|
2042
1947
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1948
|
+
LEFT JOIN grants g
|
|
1949
|
+
ON g.nest_id = n.id AND g.user_id = ?
|
|
2043
1950
|
WHERE n.user_id != ?
|
|
2044
|
-
AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL)
|
|
1951
|
+
AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL OR g.id IS NOT NULL)
|
|
2045
1952
|
ORDER BY n.created_at DESC`,
|
|
2046
|
-
[userId, userId, userId]
|
|
1953
|
+
[userId, userId, userId, userId]
|
|
2047
1954
|
);
|
|
2048
1955
|
const seen = new Set(direct.map((n) => n.id));
|
|
2049
1956
|
for (const n of await nestsSharedViaTeams(userId)) {
|
|
@@ -2069,10 +1976,16 @@ async function listSharedNests(userId) {
|
|
|
2069
1976
|
);
|
|
2070
1977
|
return direct;
|
|
2071
1978
|
}
|
|
2072
|
-
async function listVisibleNests(userId) {
|
|
1979
|
+
async function listVisibleNests(userId, opts = {}) {
|
|
2073
1980
|
const db = getDb();
|
|
2074
1981
|
const includeAnon = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
|
|
2075
|
-
const params =
|
|
1982
|
+
const params = [
|
|
1983
|
+
userId,
|
|
1984
|
+
userId,
|
|
1985
|
+
...opts.readable ? [userId] : [],
|
|
1986
|
+
userId,
|
|
1987
|
+
...includeAnon ? [ANON_USER_ID] : []
|
|
1988
|
+
];
|
|
2076
1989
|
const rows = await db.all(
|
|
2077
1990
|
`SELECT DISTINCT n.* FROM nests n
|
|
2078
1991
|
LEFT JOIN nest_collaborators nc
|
|
@@ -2081,9 +1994,11 @@ async function listVisibleNests(userId) {
|
|
|
2081
1994
|
LEFT JOIN stewards s
|
|
2082
1995
|
ON s.nest_id = n.id AND s.is_active = 1
|
|
2083
1996
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1997
|
+
${opts.readable ? "LEFT JOIN grants g ON g.nest_id = n.id AND g.user_id = ?" : ""}
|
|
2084
1998
|
WHERE n.user_id = ?
|
|
2085
1999
|
OR nc.user_id IS NOT NULL
|
|
2086
2000
|
OR s.id IS NOT NULL
|
|
2001
|
+
${opts.readable ? "OR g.id IS NOT NULL OR n.visibility = 'public'" : ""}
|
|
2087
2002
|
${includeAnon ? "OR n.user_id = ?" : ""}
|
|
2088
2003
|
ORDER BY n.created_at DESC`,
|
|
2089
2004
|
params
|
|
@@ -2140,6 +2055,435 @@ async function deleteNest(nestId) {
|
|
|
2140
2055
|
trackEvent("nest.delete", { nestId });
|
|
2141
2056
|
}
|
|
2142
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
|
+
|
|
2143
2487
|
export {
|
|
2144
2488
|
nowExpr,
|
|
2145
2489
|
insertOrIgnore,
|
|
@@ -2161,6 +2505,12 @@ export {
|
|
|
2161
2505
|
getAccessConfig,
|
|
2162
2506
|
isSuperAdmin,
|
|
2163
2507
|
isConfigSuperAdmin,
|
|
2508
|
+
collabPermToRole,
|
|
2509
|
+
canViewWith,
|
|
2510
|
+
canEditWith,
|
|
2511
|
+
canApproveWith,
|
|
2512
|
+
primaryRole,
|
|
2513
|
+
permissionLabel,
|
|
2164
2514
|
verifySmtp,
|
|
2165
2515
|
notifyEmailForNest,
|
|
2166
2516
|
sendEmailToRecipient,
|
|
@@ -2188,6 +2538,16 @@ export {
|
|
|
2188
2538
|
listPublicNests,
|
|
2189
2539
|
getNest,
|
|
2190
2540
|
deleteNest,
|
|
2541
|
+
assertSafeNodeId,
|
|
2542
|
+
folderOfId,
|
|
2543
|
+
markIndexStale,
|
|
2544
|
+
ensureNodeIndex,
|
|
2545
|
+
rebuildNodeIndex,
|
|
2546
|
+
setSuggestionFlag,
|
|
2547
|
+
nodesWithSuggestions,
|
|
2548
|
+
indexedNodeIds,
|
|
2549
|
+
readIndexedNodes,
|
|
2550
|
+
indexedFolderCounts,
|
|
2191
2551
|
engineCache,
|
|
2192
2552
|
engineApi,
|
|
2193
2553
|
opContext,
|
|
@@ -2214,12 +2574,8 @@ export {
|
|
|
2214
2574
|
shareTeamWithNest,
|
|
2215
2575
|
unshareTeamFromNest,
|
|
2216
2576
|
resolveTeamRolesForUser,
|
|
2217
|
-
collabPermToRole,
|
|
2218
|
-
canViewWith,
|
|
2219
|
-
canEditWith,
|
|
2220
|
-
canApproveWith,
|
|
2221
|
-
primaryRole,
|
|
2222
2577
|
isServerAdminUserId,
|
|
2578
|
+
resolveNestAccess,
|
|
2223
2579
|
resolveNestPermission,
|
|
2224
2580
|
permissionLevel,
|
|
2225
2581
|
isPublicReader
|