@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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  reject,
17
17
  safePublishDocument,
18
18
  submitForReview
19
- } from "./chunk-E7E3JMQR.js";
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-LO54V4AU.js";
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-5MT4ZBVF.js";
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-G62P54ET.js";
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(body.email);
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: body.email, claimed: true });
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, body.email, body.name || null, passwordHash);
324
- trackEvent("user.register", { userId, email: body.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: body.email,
340
+ email,
333
341
  name: body.name || null,
334
- is_admin: isLicenseAdminEmail(body.email)
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(body.email);
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
- let user = db.prepare("SELECT id, email, name FROM users WHERE email = ?").get(me.email);
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, me.email, me.name || null, placeholderHash);
548
- user = { id: userId, email: me.email, name: me.name || null };
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(body.email);
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, body.email, null, placeholderHash);
755
- user = { id: userId, email: body.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-OCZUV2QP.js");
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 existing = db.prepare("SELECT id FROM users WHERE email = ?").get(params.email);
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, params.email, null, await hashPassword2(uuid2()));
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-VOD5HY3I.js");
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-OCZUV2QP.js");
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-GYX3AW6E.js");
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 join2 } from "path";
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 = join2(dataRoot, "nests", nestId);
3592
+ const nestPath = join3(dataRoot, "nests", nestId);
3312
3593
  const candidates = [
3313
- join2(nestPath, "stewards.yaml"),
3314
- join2(nestPath, "stewards.yml"),
3315
- join2(nestPath, ".context", "stewards.yaml")
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 = readFileSync(candidatePath, "utf-8");
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 join3, relative } from "path";
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
- join3(HERE, "web3"),
3715
- join3(process.cwd(), "dist", "web3")
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 = relative(process.cwd(), UI_DIR_ABS) || ".";
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 join4 } from "path";
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 = join4(config.DATA_ROOT, "nests", nestId);
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-E7E3JMQR.js";
10
- import "./chunk-LO54V4AU.js";
9
+ } from "./chunk-EDJRDWPL.js";
10
+ import "./chunk-6AXBB65N.js";
11
11
  import {
12
12
  canUserApprove
13
- } from "./chunk-5MT4ZBVF.js";
14
- import "./chunk-G62P54ET.js";
13
+ } from "./chunk-SO74PQWI.js";
14
+ import "./chunk-UEHFNBNR.js";
15
15
  export {
16
16
  approve,
17
17
  canUserApprove,
@@ -20,8 +20,8 @@ import {
20
20
  syncFromConfig,
21
21
  updateSteward,
22
22
  updateStewardRole
23
- } from "./chunk-5MT4ZBVF.js";
24
- import "./chunk-G62P54ET.js";
23
+ } from "./chunk-SO74PQWI.js";
24
+ import "./chunk-UEHFNBNR.js";
25
25
  export {
26
26
  assignSteward,
27
27
  canCreateInNest,
@@ -11,8 +11,8 @@ import {
11
11
  hashContent,
12
12
  setApprovedVersion,
13
13
  systemAuthor
14
- } from "./chunk-LO54V4AU.js";
15
- import "./chunk-G62P54ET.js";
14
+ } from "./chunk-6AXBB65N.js";
15
+ import "./chunk-UEHFNBNR.js";
16
16
  export {
17
17
  SYSTEM_AUTHOR_PREFIX,
18
18
  checkConflict,