@promptowl/contextnest-community 1.2.0 → 1.3.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 +2 -2
- package/README.md +16 -0
- package/dist/{chunk-LO54V4AU.js → chunk-6AXBB65N.js} +1 -1
- package/dist/{chunk-E7E3JMQR.js → chunk-EDJRDWPL.js} +3 -3
- package/dist/{chunk-5MT4ZBVF.js → chunk-SO74PQWI.js} +130 -28
- package/dist/{chunk-G62P54ET.js → chunk-UEHFNBNR.js} +49 -5
- package/dist/index.js +330 -41
- package/dist/{review-service-GYX3AW6E.js → review-service-QU7Q7XX2.js} +4 -4
- package/dist/{stewardship-service-VOD5HY3I.js → stewardship-service-ZUSHJCNR.js} +2 -2
- package/dist/{version-service-OCZUV2QP.js → version-service-624QTR5K.js} +2 -2
- package/dist/web3/assets/index-DJH4nUEV.js +776 -0
- package/dist/web3/assets/index-uR0ua3Ak.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +1 -1
- package/dist/web3/assets/index-72vKyivD.js +0 -756
- package/dist/web3/assets/index-JmSevkg_.css +0 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
reject,
|
|
17
17
|
safePublishDocument,
|
|
18
18
|
submitForReview
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-EDJRDWPL.js";
|
|
20
20
|
import {
|
|
21
21
|
checkConflict,
|
|
22
22
|
createVersion,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
getDisplayStatus,
|
|
26
26
|
getVersions,
|
|
27
27
|
setApprovedVersion
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-6AXBB65N.js";
|
|
29
29
|
import {
|
|
30
30
|
AppError,
|
|
31
31
|
ConflictError,
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
nestAllowsSelfApprove,
|
|
68
68
|
permissionLevel,
|
|
69
69
|
removeSteward,
|
|
70
|
+
renameNest,
|
|
70
71
|
resolveNestPermission,
|
|
71
72
|
resolveStewardsForNode,
|
|
72
73
|
resolveStewardsWithFallback,
|
|
@@ -77,15 +78,16 @@ import {
|
|
|
77
78
|
startTelemetryLoop,
|
|
78
79
|
syncFromConfig,
|
|
79
80
|
trackEvent,
|
|
81
|
+
uniqueNestName,
|
|
80
82
|
updateSteward,
|
|
81
83
|
validateLicense
|
|
82
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-SO74PQWI.js";
|
|
83
85
|
import {
|
|
84
86
|
ANON_EMAIL,
|
|
85
87
|
ANON_USER_ID,
|
|
86
88
|
config,
|
|
87
89
|
getDb
|
|
88
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-UEHFNBNR.js";
|
|
89
91
|
|
|
90
92
|
// src/index.ts
|
|
91
93
|
import { serve } from "@hono/node-server";
|
|
@@ -210,6 +212,11 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
210
212
|
return c.json({ error: "Missing or invalid credentials" }, 401);
|
|
211
213
|
});
|
|
212
214
|
|
|
215
|
+
// src/shared/email.ts
|
|
216
|
+
function normalizeEmail(email) {
|
|
217
|
+
return email.trim().toLowerCase();
|
|
218
|
+
}
|
|
219
|
+
|
|
213
220
|
// src/shared/rate-limit.ts
|
|
214
221
|
var buckets = /* @__PURE__ */ new Map();
|
|
215
222
|
function liveBucket(key, cutoff) {
|
|
@@ -300,12 +307,13 @@ authRoutes.post("/register", async (c) => {
|
|
|
300
307
|
if (!body.email || !body.password) {
|
|
301
308
|
throw new ValidationError("email and password are required");
|
|
302
309
|
}
|
|
310
|
+
const email = normalizeEmail(body.email);
|
|
303
311
|
const ip = clientIp(c);
|
|
304
312
|
if (!tryConsume(`register:ip:${ip}`, REGISTER_LIMIT)) {
|
|
305
313
|
return c.json({ error: "Too many registration attempts, try again later" }, 429);
|
|
306
314
|
}
|
|
307
315
|
const db = getDb();
|
|
308
|
-
const existing = db.prepare("SELECT id, is_invited FROM users WHERE email = ?").get(
|
|
316
|
+
const existing = db.prepare("SELECT id, is_invited FROM users WHERE LOWER(email) = ?").get(email);
|
|
309
317
|
let userId;
|
|
310
318
|
const passwordHash = await hashPassword(body.password);
|
|
311
319
|
if (existing && existing.is_invited === 1) {
|
|
@@ -313,15 +321,15 @@ authRoutes.post("/register", async (c) => {
|
|
|
313
321
|
db.prepare(
|
|
314
322
|
"UPDATE users SET password_hash = ?, name = COALESCE(?, name), is_invited = 0 WHERE id = ?"
|
|
315
323
|
).run(passwordHash, body.name || null, userId);
|
|
316
|
-
trackEvent("user.register", { userId, email
|
|
324
|
+
trackEvent("user.register", { userId, email, claimed: true });
|
|
317
325
|
} else if (existing) {
|
|
318
326
|
throw new ValidationError("Email already registered");
|
|
319
327
|
} else {
|
|
320
328
|
userId = uuid();
|
|
321
329
|
db.prepare(
|
|
322
330
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
|
|
323
|
-
).run(userId,
|
|
324
|
-
trackEvent("user.register", { userId, email
|
|
331
|
+
).run(userId, email, body.name || null, passwordHash);
|
|
332
|
+
trackEvent("user.register", { userId, email });
|
|
325
333
|
}
|
|
326
334
|
const sessionId = createSession(userId, c.req.header("User-Agent"));
|
|
327
335
|
setSessionCookie(c, sessionId);
|
|
@@ -329,9 +337,9 @@ authRoutes.post("/register", async (c) => {
|
|
|
329
337
|
{
|
|
330
338
|
user: {
|
|
331
339
|
id: userId,
|
|
332
|
-
email
|
|
340
|
+
email,
|
|
333
341
|
name: body.name || null,
|
|
334
|
-
is_admin: isLicenseAdminEmail(
|
|
342
|
+
is_admin: isLicenseAdminEmail(email)
|
|
335
343
|
}
|
|
336
344
|
},
|
|
337
345
|
201
|
|
@@ -352,8 +360,8 @@ authRoutes.post("/login", async (c) => {
|
|
|
352
360
|
}
|
|
353
361
|
const db = getDb();
|
|
354
362
|
const user = db.prepare(
|
|
355
|
-
"SELECT id, email, name, password_hash, is_admin FROM users WHERE email = ?"
|
|
356
|
-
).get(
|
|
363
|
+
"SELECT id, email, name, password_hash, is_admin FROM users WHERE LOWER(email) = ?"
|
|
364
|
+
).get(emailLower);
|
|
357
365
|
const check = user ? await verifyPassword(body.password, user.password_hash) : { ok: false, needsRehash: false };
|
|
358
366
|
if (!user || !check.ok) {
|
|
359
367
|
if (hasIp) recordFailure(ipKey, LOGIN_LIMIT);
|
|
@@ -538,14 +546,15 @@ authRoutes.post("/promptowl", async (c) => {
|
|
|
538
546
|
}
|
|
539
547
|
}
|
|
540
548
|
const db = getDb();
|
|
541
|
-
|
|
549
|
+
const meEmail = normalizeEmail(me.email);
|
|
550
|
+
let user = db.prepare("SELECT id, email, name FROM users WHERE LOWER(email) = ?").get(meEmail);
|
|
542
551
|
if (!user) {
|
|
543
552
|
const userId = uuid();
|
|
544
553
|
const placeholderHash = await hashPassword(uuid());
|
|
545
554
|
db.prepare(
|
|
546
555
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
|
|
547
|
-
).run(userId,
|
|
548
|
-
user = { id: userId, email:
|
|
556
|
+
).run(userId, meEmail, me.name || null, placeholderHash);
|
|
557
|
+
user = { id: userId, email: meEmail, name: me.name || null };
|
|
549
558
|
trackEvent("user.register", {
|
|
550
559
|
userId,
|
|
551
560
|
email: me.email,
|
|
@@ -727,6 +736,7 @@ authRoutes.delete("/users/:userId", async (c) => {
|
|
|
727
736
|
authRoutes.post("/invite", async (c) => {
|
|
728
737
|
const body = await c.req.json();
|
|
729
738
|
if (!body.email) throw new ValidationError("email is required");
|
|
739
|
+
const email = normalizeEmail(body.email);
|
|
730
740
|
const callerId = resolveCallerUserId(c);
|
|
731
741
|
if (!callerId) {
|
|
732
742
|
return c.json(
|
|
@@ -745,14 +755,14 @@ authRoutes.post("/invite", async (c) => {
|
|
|
745
755
|
403
|
|
746
756
|
);
|
|
747
757
|
}
|
|
748
|
-
let user = db.prepare("SELECT id, email FROM users WHERE email = ?").get(
|
|
758
|
+
let user = db.prepare("SELECT id, email FROM users WHERE LOWER(email) = ?").get(email);
|
|
749
759
|
if (!user) {
|
|
750
760
|
const userId = uuid();
|
|
751
761
|
const placeholderHash = await hashPassword(uuid());
|
|
752
762
|
db.prepare(
|
|
753
763
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
|
|
754
|
-
).run(userId,
|
|
755
|
-
user = { id: userId, email
|
|
764
|
+
).run(userId, email, null, placeholderHash);
|
|
765
|
+
user = { id: userId, email };
|
|
756
766
|
}
|
|
757
767
|
const apiKey = generateApiKey();
|
|
758
768
|
const keyId = uuid();
|
|
@@ -1069,7 +1079,7 @@ async function approveExternalEdit(input) {
|
|
|
1069
1079
|
const node = await storage.readDocument(input.documentId);
|
|
1070
1080
|
const versionNum = result.versionEntry.version;
|
|
1071
1081
|
const tags = node.frontmatter.tags || [];
|
|
1072
|
-
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-
|
|
1082
|
+
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-624QTR5K.js");
|
|
1073
1083
|
createVersion2({
|
|
1074
1084
|
nestId: input.nestId,
|
|
1075
1085
|
nodeId: input.documentId,
|
|
@@ -1449,6 +1459,185 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1449
1459
|
return { node, version: responseVersion };
|
|
1450
1460
|
}
|
|
1451
1461
|
|
|
1462
|
+
// src/nests/unsynced-service.ts
|
|
1463
|
+
import { readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
1464
|
+
import { join as join2, relative } from "path";
|
|
1465
|
+
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
1466
|
+
function scanMarkdown(dir) {
|
|
1467
|
+
let count = 0;
|
|
1468
|
+
let size = 0;
|
|
1469
|
+
let entries = [];
|
|
1470
|
+
try {
|
|
1471
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1472
|
+
} catch {
|
|
1473
|
+
return { count, size };
|
|
1474
|
+
}
|
|
1475
|
+
for (const e of entries) {
|
|
1476
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
1477
|
+
const full = join2(dir, e.name);
|
|
1478
|
+
if (e.isDirectory()) {
|
|
1479
|
+
const sub = scanMarkdown(full);
|
|
1480
|
+
count += sub.count;
|
|
1481
|
+
size += sub.size;
|
|
1482
|
+
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
1483
|
+
try {
|
|
1484
|
+
size += statSync(full).size;
|
|
1485
|
+
count++;
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return { count, size };
|
|
1491
|
+
}
|
|
1492
|
+
function hasDirectMarkdown(dir) {
|
|
1493
|
+
let entries = [];
|
|
1494
|
+
try {
|
|
1495
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1496
|
+
} catch {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
return entries.some(
|
|
1500
|
+
(e) => e.isFile() && e.name.toLowerCase().endsWith(".md")
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
function collectLeafFolders(rel, abs) {
|
|
1504
|
+
if (hasDirectMarkdown(abs)) {
|
|
1505
|
+
const { count, size } = scanMarkdown(abs);
|
|
1506
|
+
const segments = rel.split("/").filter(Boolean);
|
|
1507
|
+
return [
|
|
1508
|
+
{
|
|
1509
|
+
name: rel,
|
|
1510
|
+
label: segments[segments.length - 1] || rel,
|
|
1511
|
+
mdCount: count,
|
|
1512
|
+
sizeBytes: size
|
|
1513
|
+
}
|
|
1514
|
+
];
|
|
1515
|
+
}
|
|
1516
|
+
let entries = [];
|
|
1517
|
+
try {
|
|
1518
|
+
entries = readdirSync(abs, { withFileTypes: true });
|
|
1519
|
+
} catch {
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
const out = [];
|
|
1523
|
+
for (const e of entries) {
|
|
1524
|
+
if (!e.isDirectory()) continue;
|
|
1525
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
1526
|
+
out.push(
|
|
1527
|
+
...collectLeafFolders(`${rel}/${e.name}`, join2(abs, e.name))
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
return out;
|
|
1531
|
+
}
|
|
1532
|
+
function listUnsyncedFolders() {
|
|
1533
|
+
const root = config.DATA_ROOT;
|
|
1534
|
+
let entries = [];
|
|
1535
|
+
try {
|
|
1536
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
1537
|
+
} catch {
|
|
1538
|
+
return [];
|
|
1539
|
+
}
|
|
1540
|
+
const out = [];
|
|
1541
|
+
for (const e of entries) {
|
|
1542
|
+
if (!e.isDirectory()) continue;
|
|
1543
|
+
if (e.name.startsWith(".")) continue;
|
|
1544
|
+
if (RESERVED.has(e.name)) continue;
|
|
1545
|
+
out.push(...collectLeafFolders(e.name, join2(root, e.name)));
|
|
1546
|
+
}
|
|
1547
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1548
|
+
console.log(
|
|
1549
|
+
`[unsynced] discovered ${out.length} candidate folder(s) under ${root}`
|
|
1550
|
+
);
|
|
1551
|
+
return out;
|
|
1552
|
+
}
|
|
1553
|
+
function assertSafeFolderName(name) {
|
|
1554
|
+
if (!name || name.includes("\\")) {
|
|
1555
|
+
throw new ValidationError("Invalid folder name");
|
|
1556
|
+
}
|
|
1557
|
+
const segments = name.split("/").filter(Boolean);
|
|
1558
|
+
if (segments.length === 0) {
|
|
1559
|
+
throw new ValidationError("Invalid folder name");
|
|
1560
|
+
}
|
|
1561
|
+
if (RESERVED.has(segments[0])) {
|
|
1562
|
+
throw new ValidationError("Folder is not eligible for sync");
|
|
1563
|
+
}
|
|
1564
|
+
for (const seg of segments) {
|
|
1565
|
+
if (seg === ".." || seg.startsWith(".") || seg === "node_modules") {
|
|
1566
|
+
throw new ValidationError("Folder is not eligible for sync");
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function collectMarkdownFiles(dir, root) {
|
|
1571
|
+
const out = [];
|
|
1572
|
+
let entries = [];
|
|
1573
|
+
try {
|
|
1574
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1575
|
+
} catch {
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1578
|
+
for (const e of entries) {
|
|
1579
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
1580
|
+
const full = join2(dir, e.name);
|
|
1581
|
+
if (e.isDirectory()) {
|
|
1582
|
+
out.push(...collectMarkdownFiles(full, root));
|
|
1583
|
+
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
1584
|
+
try {
|
|
1585
|
+
const rel = relative(root, full);
|
|
1586
|
+
out.push({ path: rel, content: readFileSync(full, "utf-8") });
|
|
1587
|
+
} catch {
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return out;
|
|
1592
|
+
}
|
|
1593
|
+
async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
1594
|
+
console.log(
|
|
1595
|
+
`[unsynced] sync requested folder="${folderName}" user="${callerEmail}"`
|
|
1596
|
+
);
|
|
1597
|
+
assertSafeFolderName(folderName);
|
|
1598
|
+
const src = join2(config.DATA_ROOT, folderName);
|
|
1599
|
+
let stat;
|
|
1600
|
+
try {
|
|
1601
|
+
stat = statSync(src);
|
|
1602
|
+
} catch {
|
|
1603
|
+
console.warn(`[unsynced] source missing: ${src}`);
|
|
1604
|
+
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
1605
|
+
}
|
|
1606
|
+
if (!stat.isDirectory()) {
|
|
1607
|
+
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
1608
|
+
}
|
|
1609
|
+
const files = collectMarkdownFiles(src, src);
|
|
1610
|
+
console.log(
|
|
1611
|
+
`[unsynced] collected ${files.length} markdown file(s) from ${src}`
|
|
1612
|
+
);
|
|
1613
|
+
if (files.length === 0) {
|
|
1614
|
+
throw new ValidationError(`Folder has no markdown to sync: ${folderName}`);
|
|
1615
|
+
}
|
|
1616
|
+
const segments = folderName.split("/").filter(Boolean);
|
|
1617
|
+
const baseName = segments[segments.length - 1] || folderName;
|
|
1618
|
+
const nestName = uniqueNestName(userId, baseName);
|
|
1619
|
+
if (nestName !== baseName) {
|
|
1620
|
+
console.log(
|
|
1621
|
+
`[unsynced] name "${baseName}" already in use, using "${nestName}" instead`
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
const nest = await importNest(userId, nestName, files);
|
|
1625
|
+
console.log(
|
|
1626
|
+
`[unsynced] nest created id=${nest.id} name="${nest.name}" from folder="${folderName}"`
|
|
1627
|
+
);
|
|
1628
|
+
const documents = await registerImportedDocuments(nest.id, callerEmail);
|
|
1629
|
+
console.log(
|
|
1630
|
+
`[unsynced] registered ${documents} document(s) for nest ${nest.id}`
|
|
1631
|
+
);
|
|
1632
|
+
try {
|
|
1633
|
+
rmSync(src, { recursive: true, force: true });
|
|
1634
|
+
console.log(`[unsynced] removed source folder ${src}`);
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
console.error("[unsynced] failed to remove source folder", src, err);
|
|
1637
|
+
}
|
|
1638
|
+
return { nest, documents };
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1452
1641
|
// src/nests/routes.ts
|
|
1453
1642
|
function effectivePermission(nestId, userId) {
|
|
1454
1643
|
if (config.AUTH_MODE === "open") {
|
|
@@ -1511,6 +1700,29 @@ nestRoutes.post("/import", async (c) => {
|
|
|
1511
1700
|
);
|
|
1512
1701
|
return c.json({ nest, documents }, 201);
|
|
1513
1702
|
});
|
|
1703
|
+
nestRoutes.get("/unsynced", async (c) => {
|
|
1704
|
+
const userId = c.get("userId");
|
|
1705
|
+
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
1706
|
+
throw new ForbiddenError("Only the server admin can list unsynced folders");
|
|
1707
|
+
}
|
|
1708
|
+
return c.json({ folders: listUnsyncedFolders() });
|
|
1709
|
+
});
|
|
1710
|
+
nestRoutes.post("/unsynced/sync", async (c) => {
|
|
1711
|
+
const userId = c.get("userId");
|
|
1712
|
+
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
1713
|
+
throw new ForbiddenError("Only the server admin can sync folders");
|
|
1714
|
+
}
|
|
1715
|
+
const body = await c.req.json();
|
|
1716
|
+
if (!body?.name) {
|
|
1717
|
+
throw new ValidationError("name is required");
|
|
1718
|
+
}
|
|
1719
|
+
const result = await syncUnsyncedFolder(
|
|
1720
|
+
userId,
|
|
1721
|
+
body.name,
|
|
1722
|
+
resolveCallerEmail(userId)
|
|
1723
|
+
);
|
|
1724
|
+
return c.json(result, 201);
|
|
1725
|
+
});
|
|
1514
1726
|
nestRoutes.get("/:nestId", async (c) => {
|
|
1515
1727
|
const nestId = c.req.param("nestId");
|
|
1516
1728
|
const userId = c.get("userId");
|
|
@@ -1527,6 +1739,25 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
1527
1739
|
const nest = getNest(nestId);
|
|
1528
1740
|
return c.json({ nest, permission, roles, myStewards });
|
|
1529
1741
|
});
|
|
1742
|
+
nestRoutes.patch("/:nestId", async (c) => {
|
|
1743
|
+
const nestId = c.req.param("nestId");
|
|
1744
|
+
const userId = c.get("userId");
|
|
1745
|
+
const permission = effectivePermission(nestId, userId);
|
|
1746
|
+
if (permission === "none") {
|
|
1747
|
+
throw new NotFoundError("Nest not found");
|
|
1748
|
+
}
|
|
1749
|
+
if (permission !== "owner" && permission !== "admin") {
|
|
1750
|
+
throw new ForbiddenError(
|
|
1751
|
+
"Only the nest owner or an admin can rename a nest."
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const body = await c.req.json();
|
|
1755
|
+
const nest = renameNest(nestId, {
|
|
1756
|
+
name: body.name,
|
|
1757
|
+
description: body.description
|
|
1758
|
+
});
|
|
1759
|
+
return c.json({ nest });
|
|
1760
|
+
});
|
|
1530
1761
|
nestRoutes.delete("/:nestId", async (c) => {
|
|
1531
1762
|
const nestId = c.req.param("nestId");
|
|
1532
1763
|
const userId = c.get("userId");
|
|
@@ -1602,7 +1833,8 @@ async function addCollaborator(params) {
|
|
|
1602
1833
|
const db = getDb();
|
|
1603
1834
|
let userId = params.userId;
|
|
1604
1835
|
if (!userId && params.email) {
|
|
1605
|
-
const
|
|
1836
|
+
const email = normalizeEmail(params.email);
|
|
1837
|
+
const existing = db.prepare("SELECT id FROM users WHERE LOWER(email) = ?").get(email);
|
|
1606
1838
|
if (existing) {
|
|
1607
1839
|
userId = existing.id;
|
|
1608
1840
|
} else {
|
|
@@ -1610,7 +1842,7 @@ async function addCollaborator(params) {
|
|
|
1610
1842
|
userId = uuid2();
|
|
1611
1843
|
db.prepare(
|
|
1612
1844
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
|
|
1613
|
-
).run(userId,
|
|
1845
|
+
).run(userId, email, null, await hashPassword2(uuid2()));
|
|
1614
1846
|
}
|
|
1615
1847
|
}
|
|
1616
1848
|
if (!userId) {
|
|
@@ -1781,7 +2013,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
1781
2013
|
nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
1782
2014
|
const nestId = c.req.param("nestId");
|
|
1783
2015
|
const nodeId = c.req.param("nodeId");
|
|
1784
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
2016
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-ZUSHJCNR.js");
|
|
1785
2017
|
const { stewards, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback2(
|
|
1786
2018
|
nestId,
|
|
1787
2019
|
nodeId
|
|
@@ -1802,7 +2034,7 @@ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
|
1802
2034
|
nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
1803
2035
|
const nestId = c.req.param("nestId");
|
|
1804
2036
|
const nodeId = c.req.param("nodeId");
|
|
1805
|
-
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-
|
|
2037
|
+
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-624QTR5K.js");
|
|
1806
2038
|
const allVersions = getVersions2(nestId, nodeId);
|
|
1807
2039
|
const approved = getApprovedVersion2(nestId, nodeId);
|
|
1808
2040
|
const db = getDb();
|
|
@@ -1833,7 +2065,7 @@ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
|
1833
2065
|
nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
|
|
1834
2066
|
const nestId = c.req.param("nestId");
|
|
1835
2067
|
const nodeId = c.req.param("nodeId");
|
|
1836
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
2068
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-QU7Q7XX2.js");
|
|
1837
2069
|
const history = getReviewHistory2(nestId, nodeId);
|
|
1838
2070
|
return c.json({ reviews: history });
|
|
1839
2071
|
});
|
|
@@ -2798,6 +3030,26 @@ var TOOL_DEFINITIONS = [
|
|
|
2798
3030
|
},
|
|
2799
3031
|
required: ["email"]
|
|
2800
3032
|
}
|
|
3033
|
+
},
|
|
3034
|
+
// ─── Unsynced Folder Tools ─────────────────────────────────────────
|
|
3035
|
+
{
|
|
3036
|
+
name: "context_unsynced_list",
|
|
3037
|
+
description: "List folders under the data root that are sync candidates \u2014 dropped in by the starter CLI or copied in manually, not yet converted into a nest. Server-admin only in non-open mode.",
|
|
3038
|
+
inputSchema: { type: "object", properties: {} }
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
name: "context_sync_folder",
|
|
3042
|
+
description: "Convert a sibling folder under the data root into a real nest. Imports its markdown, seeds governance, and removes the source folder. Use context_unsynced_list to discover folder names. Server-admin only in non-open mode.",
|
|
3043
|
+
inputSchema: {
|
|
3044
|
+
type: "object",
|
|
3045
|
+
properties: {
|
|
3046
|
+
name: {
|
|
3047
|
+
type: "string",
|
|
3048
|
+
description: "Folder path relative to the data root (e.g. 'nodes/architecture'), as returned by context_unsynced_list."
|
|
3049
|
+
}
|
|
3050
|
+
},
|
|
3051
|
+
required: ["name"]
|
|
3052
|
+
}
|
|
2801
3053
|
}
|
|
2802
3054
|
];
|
|
2803
3055
|
async function resolveLlmBody(ctx, node) {
|
|
@@ -2812,7 +3064,7 @@ async function resolveLlmBody(ctx, node) {
|
|
|
2812
3064
|
}
|
|
2813
3065
|
}
|
|
2814
3066
|
async function handleToolCall(toolName, args, ctx) {
|
|
2815
|
-
const { storage, queryEngine, versionManager, nestId, userEmail } = ctx;
|
|
3067
|
+
const { storage, queryEngine, versionManager, nestId, userId, userEmail } = ctx;
|
|
2816
3068
|
switch (toolName) {
|
|
2817
3069
|
case "context_init": {
|
|
2818
3070
|
const content = await storage.readContextMd();
|
|
@@ -3174,6 +3426,34 @@ ${list}`;
|
|
|
3174
3426
|
return `Failed to share nest: ${err.message}`;
|
|
3175
3427
|
}
|
|
3176
3428
|
}
|
|
3429
|
+
case "context_unsynced_list": {
|
|
3430
|
+
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
3431
|
+
return "You don't have permission to list unsynced folders. Server admin only.";
|
|
3432
|
+
}
|
|
3433
|
+
const folders = listUnsyncedFolders();
|
|
3434
|
+
if (!folders.length) return "No unsynced folders found.";
|
|
3435
|
+
const list = folders.map(
|
|
3436
|
+
(f, i) => `${i + 1}. **${f.label}** \`${f.name}\` \u2014 ${f.mdCount} md file(s), ${f.sizeBytes} bytes`
|
|
3437
|
+
).join("\n");
|
|
3438
|
+
return `# Unsynced Folders (${folders.length})
|
|
3439
|
+
|
|
3440
|
+
${list}`;
|
|
3441
|
+
}
|
|
3442
|
+
case "context_sync_folder": {
|
|
3443
|
+
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
3444
|
+
return "You don't have permission to sync folders. Server admin only.";
|
|
3445
|
+
}
|
|
3446
|
+
try {
|
|
3447
|
+
const { nest, documents } = await syncUnsyncedFolder(
|
|
3448
|
+
userId,
|
|
3449
|
+
args.name,
|
|
3450
|
+
userEmail
|
|
3451
|
+
);
|
|
3452
|
+
return `Synced folder "${args.name}" \u2192 nest **${nest.name}** (${nest.id}) with ${documents} document(s).`;
|
|
3453
|
+
} catch (err) {
|
|
3454
|
+
return `Failed to sync folder: ${err.message}`;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3177
3457
|
default:
|
|
3178
3458
|
return `Unknown tool: ${toolName}`;
|
|
3179
3459
|
}
|
|
@@ -3187,7 +3467,7 @@ function getUserEmail2(userId) {
|
|
|
3187
3467
|
const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
|
|
3188
3468
|
return user?.email || "anonymous@localhost";
|
|
3189
3469
|
}
|
|
3190
|
-
function createMcpServerForNest(nestId, userEmail) {
|
|
3470
|
+
function createMcpServerForNest(nestId, userId, userEmail) {
|
|
3191
3471
|
const server = new McpServer(
|
|
3192
3472
|
{ name: `contextnest-${nestId}`, version: "1.0.0" },
|
|
3193
3473
|
{ capabilities: { tools: {} } }
|
|
@@ -3212,6 +3492,7 @@ function createMcpServerForNest(nestId, userEmail) {
|
|
|
3212
3492
|
queryEngine: engine.query,
|
|
3213
3493
|
versionManager: engine.versions,
|
|
3214
3494
|
nestId,
|
|
3495
|
+
userId,
|
|
3215
3496
|
userEmail
|
|
3216
3497
|
});
|
|
3217
3498
|
return { content: [{ type: "text", text }] };
|
|
@@ -3223,7 +3504,7 @@ mcpRoutes.all("/", async (c) => {
|
|
|
3223
3504
|
const nestId = c.req.param("nestId");
|
|
3224
3505
|
const userId = c.get("userId");
|
|
3225
3506
|
const userEmail = getUserEmail2(userId);
|
|
3226
|
-
const server = createMcpServerForNest(nestId, userEmail);
|
|
3507
|
+
const server = createMcpServerForNest(nestId, userId, userEmail);
|
|
3227
3508
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
3228
3509
|
sessionIdGenerator: void 0,
|
|
3229
3510
|
enableJsonResponse: true
|
|
@@ -3242,8 +3523,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
3242
3523
|
import { Hono as Hono7 } from "hono";
|
|
3243
3524
|
|
|
3244
3525
|
// src/governance/stewards-parser.ts
|
|
3245
|
-
import { readFileSync, existsSync } from "fs";
|
|
3246
|
-
import { join as
|
|
3526
|
+
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
3527
|
+
import { join as join3 } from "path";
|
|
3247
3528
|
function parseStewardsYaml(content) {
|
|
3248
3529
|
const result = { version: 1 };
|
|
3249
3530
|
const lines = content.split("\n");
|
|
@@ -3308,15 +3589,15 @@ function parseEntry(str) {
|
|
|
3308
3589
|
}
|
|
3309
3590
|
function loadStewardsConfig(nestId) {
|
|
3310
3591
|
const dataRoot = config.DATA_ROOT;
|
|
3311
|
-
const nestPath =
|
|
3592
|
+
const nestPath = join3(dataRoot, "nests", nestId);
|
|
3312
3593
|
const candidates = [
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3594
|
+
join3(nestPath, "stewards.yaml"),
|
|
3595
|
+
join3(nestPath, "stewards.yml"),
|
|
3596
|
+
join3(nestPath, ".context", "stewards.yaml")
|
|
3316
3597
|
];
|
|
3317
3598
|
for (const candidatePath of candidates) {
|
|
3318
3599
|
if (existsSync(candidatePath)) {
|
|
3319
|
-
const content =
|
|
3600
|
+
const content = readFileSync2(candidatePath, "utf-8");
|
|
3320
3601
|
return parseStewardsYaml(content);
|
|
3321
3602
|
}
|
|
3322
3603
|
}
|
|
@@ -3707,15 +3988,15 @@ function ensureAnonymousUser() {
|
|
|
3707
3988
|
// src/app.ts
|
|
3708
3989
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3709
3990
|
import { fileURLToPath } from "url";
|
|
3710
|
-
import { dirname, join as
|
|
3991
|
+
import { dirname, join as join4, relative as relative2 } from "path";
|
|
3711
3992
|
import { existsSync as existsSync2 } from "fs";
|
|
3712
3993
|
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
3713
3994
|
var UI_DIR_CANDIDATES = [
|
|
3714
|
-
|
|
3715
|
-
|
|
3995
|
+
join4(HERE, "web3"),
|
|
3996
|
+
join4(process.cwd(), "dist", "web3")
|
|
3716
3997
|
];
|
|
3717
3998
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
3718
|
-
var UI_DIR_REL =
|
|
3999
|
+
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
3719
4000
|
var openModeMiddleware = createMiddleware2(async (c, next) => {
|
|
3720
4001
|
const anonId = ensureAnonymousUser();
|
|
3721
4002
|
c.set("userId", anonId);
|
|
@@ -3847,7 +4128,14 @@ function createApp() {
|
|
|
3847
4128
|
valid: true,
|
|
3848
4129
|
tier: info.tier,
|
|
3849
4130
|
org: info.org,
|
|
3850
|
-
limits: info.limits
|
|
4131
|
+
limits: info.limits,
|
|
4132
|
+
// The key validated and is live now, but if it couldn't be written to
|
|
4133
|
+
// disk it'll be lost on restart — surface that instead of pretending
|
|
4134
|
+
// the setup is durable (the old behavior silently swallowed this).
|
|
4135
|
+
persisted: info.persisted,
|
|
4136
|
+
...info.persisted ? {} : {
|
|
4137
|
+
warning: `License validated, but it could not be saved to disk (${info.persistError || "unknown error"}). It will be LOST on the next restart. Set ENV_FILE_PATH to a writable, persisted path (e.g. under DATA_ROOT) or provide PROMPTOWL_KEY via the container environment.`
|
|
4138
|
+
}
|
|
3851
4139
|
});
|
|
3852
4140
|
} catch (err) {
|
|
3853
4141
|
const msg = err instanceof Error ? err.message : "Failed to install key";
|
|
@@ -3882,6 +4170,7 @@ function createApp() {
|
|
|
3882
4170
|
const localPath = c.req.path.replace(/^\/nests\//, "");
|
|
3883
4171
|
const parts = localPath.split("/").filter(Boolean);
|
|
3884
4172
|
if (parts.length < 2) return next();
|
|
4173
|
+
if (parts[0] === "unsynced") return next();
|
|
3885
4174
|
const nestId = parts[0];
|
|
3886
4175
|
const userId = c.get("userId");
|
|
3887
4176
|
const nestScope = c.get("nestScope");
|
|
@@ -4065,7 +4354,7 @@ function createApp() {
|
|
|
4065
4354
|
|
|
4066
4355
|
// src/db/backfill.ts
|
|
4067
4356
|
import { NestStorage } from "@promptowl/contextnest-engine";
|
|
4068
|
-
import { join as
|
|
4357
|
+
import { join as join5 } from "path";
|
|
4069
4358
|
var MIGRATION_ID = "005_backfill_node_versions_from_history";
|
|
4070
4359
|
async function backfillNodeVersionsFromHistory(db) {
|
|
4071
4360
|
const already = db.prepare("SELECT id FROM schema_migrations WHERE id = ?").get(MIGRATION_ID);
|
|
@@ -4086,7 +4375,7 @@ async function backfillNodeVersionsFromHistory(db) {
|
|
|
4086
4375
|
let totalInserted = 0;
|
|
4087
4376
|
let totalDocs = 0;
|
|
4088
4377
|
for (const { id: nestId } of nests) {
|
|
4089
|
-
const nestPath =
|
|
4378
|
+
const nestPath = join5(config.DATA_ROOT, "nests", nestId);
|
|
4090
4379
|
const storage = new NestStorage(nestPath);
|
|
4091
4380
|
let docs;
|
|
4092
4381
|
try {
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
getReviewQueue,
|
|
7
7
|
reject,
|
|
8
8
|
submitForReview
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-EDJRDWPL.js";
|
|
10
|
+
import "./chunk-6AXBB65N.js";
|
|
11
11
|
import {
|
|
12
12
|
canUserApprove
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
13
|
+
} from "./chunk-SO74PQWI.js";
|
|
14
|
+
import "./chunk-UEHFNBNR.js";
|
|
15
15
|
export {
|
|
16
16
|
approve,
|
|
17
17
|
canUserApprove,
|