@promptowl/contextnest-community 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +6 -2
  2. package/dist/{chunk-UXJSO54R.js → chunk-34UXRZXD.js} +59 -14
  3. package/dist/chunk-F66VCBLA.js +136 -0
  4. package/dist/{chunk-SNFGU5Q7.js → chunk-FUUJG6XP.js} +597 -326
  5. package/dist/{chunk-4YGJSUJC.js → chunk-HGN45I5P.js} +29 -21
  6. package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
  7. package/dist/chunk-OOBZG6BW.js +379 -0
  8. package/dist/{chunk-HWDPCPYT.js → chunk-QUZXRLUA.js} +1 -1
  9. package/dist/{chunk-YF4OFT2V.js → chunk-TQACHVVA.js} +35 -4
  10. package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
  11. package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
  12. package/dist/engine-34E3YNX7.js +14 -0
  13. package/dist/external-edit-service-6Q6SHEOF.js +33 -0
  14. package/dist/{grants-service-4NXPBYWD.js → grants-service-ME7HIVHU.js} +3 -3
  15. package/dist/index.js +496 -470
  16. package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
  17. package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
  18. package/dist/{review-service-3CCKDO5N.js → review-service-H2M5ACPQ.js} +7 -7
  19. package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-U4CKL7Y5.js} +8 -4
  20. package/dist/{version-service-EFZGGL3Y.js → version-service-JWEN5UUW.js} +6 -4
  21. package/dist/web3/assets/index-DONv5_Ni.css +1 -0
  22. package/dist/web3/assets/index-jSGrnboh.js +1065 -0
  23. package/dist/web3/index.html +2 -2
  24. package/package.json +3 -3
  25. package/dist/chunk-XRK6SQSC.js +0 -58
  26. package/dist/web3/assets/index-D1299AIf.css +0 -1
  27. package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
package/dist/index.js CHANGED
@@ -41,7 +41,27 @@ import {
41
41
  submitForReview,
42
42
  withNodeWriteLock,
43
43
  withdrawDeletionRequest
44
- } from "./chunk-4YGJSUJC.js";
44
+ } from "./chunk-HGN45I5P.js";
45
+ import {
46
+ canCreateInNest,
47
+ canManageStewards,
48
+ canUserAccess,
49
+ canUserApprove,
50
+ canUserEdit,
51
+ createStewardRecord,
52
+ getCollaboratorRole,
53
+ getStewardRolesForUser,
54
+ getStewardsForNest,
55
+ getStewardsForUser,
56
+ listStewards,
57
+ removeSteward,
58
+ resolveGrantRoles,
59
+ resolveStewardsForNode,
60
+ resolveStewardsWithFallback,
61
+ resolveUserRoles,
62
+ syncFromConfig,
63
+ updateSteward
64
+ } from "./chunk-TQACHVVA.js";
45
65
  import {
46
66
  createGrant,
47
67
  deleteGrant,
@@ -50,15 +70,16 @@ import {
50
70
  listGrants,
51
71
  listUserGrants,
52
72
  resolveNodeGrant
53
- } from "./chunk-HWDPCPYT.js";
73
+ } from "./chunk-QUZXRLUA.js";
54
74
  import {
55
75
  generateApiKey,
56
76
  getKeyPrefix,
57
77
  hashApiKey,
58
78
  hashPassword,
59
79
  parseBearerToken,
80
+ verifyKeyRequest,
60
81
  verifyPassword
61
- } from "./chunk-XRK6SQSC.js";
82
+ } from "./chunk-F66VCBLA.js";
62
83
  import {
63
84
  checkConflict,
64
85
  createVersion,
@@ -66,36 +87,32 @@ import {
66
87
  getApprovedVersions,
67
88
  getCurrentVersion,
68
89
  getDisplayStatus,
90
+ getDisplayStatuses,
69
91
  getTrackedNodeIds,
70
92
  getVersions,
71
93
  listMyDrafts,
72
94
  setApprovedVersion,
73
95
  upsertVersion
74
- } from "./chunk-UXJSO54R.js";
96
+ } from "./chunk-34UXRZXD.js";
75
97
  import {
76
- canCreateInNest,
77
- canManageStewards,
78
- canUserAccess,
79
- canUserApprove,
80
- canUserEdit,
81
- createStewardRecord,
82
- getCollaboratorRole,
83
- getStewardRolesForUser,
84
- getStewardsForNest,
85
- getStewardsForUser,
86
- listStewards,
87
- removeSteward,
88
- resolveGrantRoles,
89
- resolveStewardsForNode,
90
- resolveStewardsWithFallback,
91
- resolveUserRoles,
92
- syncFromConfig,
93
- updateSteward
94
- } from "./chunk-YF4OFT2V.js";
98
+ approveExternalEdit,
99
+ getExternalEditDetail,
100
+ getPendingChange,
101
+ listExternalEditVerdicts,
102
+ listNestExternalEdits,
103
+ loadLatestApprovedNode,
104
+ rejectExternalEdit,
105
+ scanDocumentForDrift,
106
+ scanNestForDrift,
107
+ startDriftScanner
108
+ } from "./chunk-OOBZG6BW.js";
95
109
  import {
96
110
  addMember,
111
+ assertSafeNodeId,
97
112
  buildTitleMap,
98
113
  canApproveWith,
114
+ canEditWith,
115
+ collabPermToRole,
99
116
  createNest,
100
117
  createTeam,
101
118
  deleteNest,
@@ -104,14 +121,17 @@ import {
104
121
  docLink,
105
122
  engineApi,
106
123
  engineCache,
124
+ ensureNodeIndex,
107
125
  findTeamByExternalId,
108
126
  folderForNode,
127
+ folderOfId,
109
128
  getAccessConfig,
110
129
  getCurrentLicense,
111
130
  getNest,
112
131
  getSuspensionReason,
113
132
  getTeam,
114
133
  importNest,
134
+ indexedFolderCounts,
115
135
  insertOrIgnore,
116
136
  installLicenseKey,
117
137
  isConfigSuperAdmin,
@@ -136,11 +156,15 @@ import {
136
156
  nestPrimeTags,
137
157
  nestReviewsPrimeOnly,
138
158
  nestStorageRoot,
159
+ nodesWithSuggestions,
139
160
  normalizeTag,
140
161
  nowExpr,
141
162
  opContext,
142
163
  permissionLevel,
143
164
  persistSetting,
165
+ primaryRole,
166
+ readIndexedNodes,
167
+ rebuildNodeIndex,
144
168
  removeMember,
145
169
  removeNodeFromTagIndex,
146
170
  renameNest,
@@ -150,6 +174,7 @@ import {
150
174
  resolveNestAccess,
151
175
  resolveNestPath,
152
176
  resolveNestPermission,
177
+ resolveTeamRolesForUser,
153
178
  sendEmailToRecipient,
154
179
  setAllowSelfApprove,
155
180
  setPrimeOnlyReview,
@@ -165,7 +190,7 @@ import {
165
190
  updateMemberRole,
166
191
  validateLicense,
167
192
  verifySmtp
168
- } from "./chunk-SNFGU5Q7.js";
193
+ } from "./chunk-FUUJG6XP.js";
169
194
  import {
170
195
  AppError,
171
196
  ConflictError,
@@ -181,11 +206,12 @@ import {
181
206
  initDb,
182
207
  isEmailListish,
183
208
  isEmailish
184
- } from "./chunk-JMJLNEXE.js";
209
+ } from "./chunk-HYSTFMFG.js";
185
210
  import {
186
211
  ANON_EMAIL,
187
- ANON_USER_ID
188
- } from "./chunk-SLTQACJW.js";
212
+ ANON_USER_ID,
213
+ MAX_FOLDER_DEPTH
214
+ } from "./chunk-YB3LKF7U.js";
189
215
  import {
190
216
  normalizeEmail
191
217
  } from "./chunk-FRQJWGN3.js";
@@ -1079,19 +1105,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
1079
1105
  );
1080
1106
  const db = getDb();
1081
1107
  const userId = c.get("userId");
1082
- const existing = await db.get(
1083
- "SELECT key_prefix FROM api_keys WHERE user_id = ?",
1084
- [userId]
1085
- );
1086
- if (existing) {
1087
- return c.json(
1088
- {
1089
- error: "An API key already exists for this user. Rotate it instead.",
1090
- key_prefix: existing.key_prefix
1091
- },
1092
- 409
1093
- );
1094
- }
1108
+ const check = await verifyKeyRequest(db, userId, {
1109
+ action: "generate",
1110
+ label: body.label
1111
+ });
1112
+ if (!check.ok) return c.json({ error: check.error }, check.status);
1095
1113
  const apiKey = generateApiKey();
1096
1114
  const keyId = uuid2();
1097
1115
  await db.run(
@@ -1102,11 +1120,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
1102
1120
  hashApiKey(apiKey),
1103
1121
  getKeyPrefix(apiKey),
1104
1122
  body.nest_id || null,
1105
- body.label || null
1123
+ check.label
1106
1124
  ]
1107
1125
  );
1108
1126
  return c.json(
1109
- { api_key: apiKey, key_prefix: getKeyPrefix(apiKey) },
1127
+ { id: keyId, api_key: apiKey, key_prefix: getKeyPrefix(apiKey), label: check.label },
1110
1128
  201
1111
1129
  );
1112
1130
  });
@@ -1114,15 +1132,22 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
1114
1132
  const body = await c.req.json().catch(() => ({}));
1115
1133
  const db = getDb();
1116
1134
  const userId = c.get("userId");
1135
+ const check = await verifyKeyRequest(db, userId, {
1136
+ action: "update",
1137
+ label: body.label,
1138
+ keyId: body.key_id
1139
+ });
1140
+ if (!check.ok) return c.json({ error: check.error }, check.status);
1141
+ const prior = check.key;
1117
1142
  const apiKey = generateApiKey();
1118
1143
  const keyId = uuid2();
1119
- const prior = await db.get(
1120
- "SELECT label, nest_id FROM api_keys WHERE user_id = ?",
1121
- [userId]
1122
- );
1123
- const nestScope = "nest_id" in body ? body.nest_id || null : prior?.nest_id ?? null;
1124
- await db.transaction(async (tx) => {
1125
- await tx.run("DELETE FROM api_keys WHERE user_id = ?", [userId]);
1144
+ const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
1145
+ const replaced = await db.transaction(async (tx) => {
1146
+ const removed = await tx.run(
1147
+ "DELETE FROM api_keys WHERE id = ? AND user_id = ?",
1148
+ [prior.id, userId]
1149
+ );
1150
+ if (removed.changes === 0) return false;
1126
1151
  await tx.run(
1127
1152
  "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
1128
1153
  [
@@ -1131,29 +1156,47 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
1131
1156
  hashApiKey(apiKey),
1132
1157
  getKeyPrefix(apiKey),
1133
1158
  nestScope,
1134
- body.label ?? prior?.label ?? null
1159
+ check.label ?? prior.label
1135
1160
  ]
1136
1161
  );
1162
+ return true;
1163
+ });
1164
+ if (!replaced) {
1165
+ return c.json(
1166
+ { error: "That key no longer exists \u2014 reload and try again." },
1167
+ 404
1168
+ );
1169
+ }
1170
+ return c.json({
1171
+ id: keyId,
1172
+ api_key: apiKey,
1173
+ key_prefix: getKeyPrefix(apiKey),
1174
+ label: check.label ?? prior.label
1137
1175
  });
1138
- return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
1139
1176
  });
1140
1177
  authRoutes.get("/keys", authMiddleware, async (c) => {
1141
1178
  const db = getDb();
1179
+ const userId = c.get("userId");
1180
+ const check = await verifyKeyRequest(db, userId, { action: "fetch" });
1181
+ if (!check.ok) return c.json({ error: check.error }, check.status);
1142
1182
  const keys = await db.all(
1143
- "SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?",
1144
- [c.get("userId")]
1183
+ "SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ? ORDER BY created_at ASC, id ASC",
1184
+ [userId]
1145
1185
  );
1146
1186
  return c.json({ keys });
1147
1187
  });
1148
1188
  authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
1149
1189
  const db = getDb();
1150
- const result = await db.run(
1151
- "DELETE FROM api_keys WHERE id = ? AND user_id = ?",
1152
- [c.req.param("keyId"), c.get("userId")]
1153
- );
1154
- if (result.changes === 0) {
1155
- return c.json({ error: "Key not found" }, 404);
1156
- }
1190
+ const userId = c.get("userId");
1191
+ const check = await verifyKeyRequest(db, userId, {
1192
+ action: "update",
1193
+ keyId: c.req.param("keyId")
1194
+ });
1195
+ if (!check.ok) return c.json({ error: check.error }, check.status);
1196
+ await db.run("DELETE FROM api_keys WHERE id = ? AND user_id = ?", [
1197
+ check.key.id,
1198
+ userId
1199
+ ]);
1157
1200
  return c.json({ deleted: true });
1158
1201
  });
1159
1202
  authRoutes.post("/device", async (c) => {
@@ -1610,13 +1653,14 @@ import { Hono as Hono3 } from "hono";
1610
1653
 
1611
1654
  // src/nodes/service.ts
1612
1655
  import {
1613
- parseDocument as parseDocument2,
1656
+ parseDocument,
1614
1657
  normalizeStatus,
1615
1658
  isRejected,
1659
+ filterDocuments,
1616
1660
  DocumentNotFoundError
1617
1661
  } from "@promptowl/contextnest-engine";
1618
1662
  import { rename, mkdir } from "fs/promises";
1619
- import { dirname, join as join2 } from "path";
1663
+ import { dirname, join } from "path";
1620
1664
 
1621
1665
  // src/governance/safe-publish.ts
1622
1666
  import {
@@ -1644,6 +1688,9 @@ function stripUndefinedDeep(value) {
1644
1688
  return value;
1645
1689
  }
1646
1690
 
1691
+ // src/nodes/service.ts
1692
+ import { inputJsonSchema } from "@promptowl/contextnest-engine/api";
1693
+
1647
1694
  // src/governance/prime-service.ts
1648
1695
  async function isPrimeDocument(nestId, tags, metadata) {
1649
1696
  const flag = documentPrimeFlag(metadata);
@@ -1661,313 +1708,6 @@ function documentPrimeFlag(metadata) {
1661
1708
  return void 0;
1662
1709
  }
1663
1710
 
1664
- // src/governance/external-edit-service.ts
1665
- import { readFile } from "fs/promises";
1666
- import { join } from "path";
1667
- import {
1668
- detectDrift,
1669
- stageSuggestion,
1670
- ChainEventLog,
1671
- approveSuggestion,
1672
- rejectSuggestion,
1673
- listSuggestions,
1674
- readSuggestion,
1675
- parseDocument,
1676
- VersionManager,
1677
- computeContentHash,
1678
- getChecksumContent
1679
- } from "@promptowl/contextnest-engine";
1680
- var communityRbac = {
1681
- isCzar: () => false,
1682
- canIngest: () => true,
1683
- isDocOwner: () => true
1684
- };
1685
- function docPath(nestId, documentId) {
1686
- return join(resolveNestPath(nestId), `${documentId}.md`);
1687
- }
1688
- async function readRaw(nestId, documentId) {
1689
- try {
1690
- return await readFile(docPath(nestId, documentId), "utf-8");
1691
- } catch {
1692
- return null;
1693
- }
1694
- }
1695
- async function loadChainHead(storage, documentId) {
1696
- const history = await storage.readHistory(documentId);
1697
- if (!history || history.versions.length === 0) return null;
1698
- const latest = history.versions[history.versions.length - 1];
1699
- try {
1700
- const content = await new VersionManager(storage).reconstructVersion(
1701
- documentId,
1702
- latest.version
1703
- );
1704
- return { version: latest.version, content };
1705
- } catch {
1706
- return null;
1707
- }
1708
- }
1709
- async function loadLatestApprovedNode(nestId, documentId) {
1710
- const { storage } = await engineCache.get(nestId);
1711
- const head = await loadChainHead(storage, documentId);
1712
- if (!head) return null;
1713
- return parseDocument(docPath(nestId, documentId), head.content, documentId);
1714
- }
1715
- async function bodyMatchesLatestVersion(storage, documentId, liveBodyHash) {
1716
- const head = await loadChainHead(storage, documentId);
1717
- if (!head) return false;
1718
- const headBodyHash = computeContentHash(getChecksumContent(head.content));
1719
- return headBodyHash === liveBodyHash;
1720
- }
1721
- async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner") {
1722
- const res = await scanDocumentForDriftInternal(nestId, documentId, actor);
1723
- return res?.meta ?? null;
1724
- }
1725
- async function scanDocumentForDriftInternal(nestId, documentId, actor) {
1726
- const { storage } = await engineCache.get(nestId);
1727
- const node = await storage.readDocument(documentId).catch(() => null);
1728
- if (!node) return null;
1729
- const raw = await readRaw(nestId, documentId);
1730
- if (raw == null) return null;
1731
- const drift = detectDrift(raw, node.frontmatter.checksum);
1732
- if (!drift.drifted) return null;
1733
- if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
1734
- return null;
1735
- }
1736
- const approved = await loadChainHead(storage, documentId);
1737
- if (!approved) return null;
1738
- const existing = await listSuggestions(storage, documentId);
1739
- const dup = existing.find((s) => s.proposed_hash === drift.actualHash);
1740
- if (dup) return { meta: dup, created: false };
1741
- const result = await stageSuggestion({
1742
- storage,
1743
- documentId,
1744
- approvedRawContent: approved.content,
1745
- proposedRawContent: raw,
1746
- source: "out-of-band-edit",
1747
- actor,
1748
- docTier: "standard"
1749
- });
1750
- return { meta: result.meta, created: true };
1751
- }
1752
- async function scanNestForDrift(nestId, actor = "system:scanner") {
1753
- const { storage, invalidateDiscovery } = await engineCache.get(nestId);
1754
- invalidateDiscovery();
1755
- const docs = await storage.discoverDocuments();
1756
- const results = await Promise.all(
1757
- docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
1758
- );
1759
- const staged = results.filter((r) => r?.created).length;
1760
- return { scanned: docs.length, staged };
1761
- }
1762
- async function getPendingChange(nestId, documentId) {
1763
- const { storage } = await engineCache.get(nestId);
1764
- const list = await listSuggestions(storage, documentId);
1765
- for (let i = list.length - 1; i >= 0; i--) {
1766
- const meta = list[i];
1767
- if (await bodyMatchesLatestVersion(storage, documentId, meta.proposed_hash)) {
1768
- continue;
1769
- }
1770
- return {
1771
- suggestion_id: meta.suggestion_id,
1772
- detected_at: meta.detected_at,
1773
- source: meta.source,
1774
- proposed_hash: meta.proposed_hash
1775
- };
1776
- }
1777
- return null;
1778
- }
1779
- async function listNestExternalEdits(nestId) {
1780
- const { storage } = await engineCache.get(nestId);
1781
- const docs = await storage.discoverDocuments();
1782
- const lists = await Promise.all(
1783
- docs.map(async (doc) => {
1784
- const metas = await listSuggestions(storage, doc.id);
1785
- const fresh = [];
1786
- for (const meta of metas) {
1787
- if (await bodyMatchesLatestVersion(storage, doc.id, meta.proposed_hash)) {
1788
- continue;
1789
- }
1790
- fresh.push(meta);
1791
- }
1792
- return fresh;
1793
- })
1794
- );
1795
- const entries = lists.flat().map((meta) => ({
1796
- suggestion_id: meta.suggestion_id,
1797
- nest_id: nestId,
1798
- document_id: meta.document_id,
1799
- source: meta.source,
1800
- detected_at: meta.detected_at,
1801
- actor: meta.actor,
1802
- target_hash: meta.target_hash,
1803
- proposed_hash: meta.proposed_hash,
1804
- note: meta.note
1805
- }));
1806
- return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
1807
- }
1808
- async function getExternalEditDetail(nestId, documentId, suggestionId) {
1809
- const { storage } = await engineCache.get(nestId);
1810
- const found = await readSuggestion(storage, documentId, suggestionId);
1811
- if (!found) return null;
1812
- return {
1813
- suggestion_id: found.meta.suggestion_id,
1814
- nest_id: nestId,
1815
- document_id: found.meta.document_id,
1816
- source: found.meta.source,
1817
- detected_at: found.meta.detected_at,
1818
- actor: found.meta.actor,
1819
- target_hash: found.meta.target_hash,
1820
- proposed_hash: found.meta.proposed_hash,
1821
- note: found.meta.note,
1822
- patch: found.patch
1823
- };
1824
- }
1825
- async function recordVerdict(nestId, event) {
1826
- try {
1827
- const { storage } = await engineCache.get(nestId);
1828
- await new ChainEventLog(storage).append(event);
1829
- return true;
1830
- } catch (err) {
1831
- console.error(
1832
- `[external-edit] failed to record verdict ${event.event_id} for nest ${nestId}:`,
1833
- err
1834
- );
1835
- return false;
1836
- }
1837
- }
1838
- async function listExternalEditVerdicts(nestId, documentId) {
1839
- try {
1840
- const { storage } = await engineCache.get(nestId);
1841
- const events = await new ChainEventLog(storage).readByDocument(documentId);
1842
- return events.flatMap((e) => {
1843
- const meta = e.action_metadata || {};
1844
- if (typeof meta.suggestion_id !== "string") return [];
1845
- const reason = meta.rejection_reason;
1846
- const comment = meta.approval_comment;
1847
- return [
1848
- {
1849
- suggestion_id: meta.suggestion_id,
1850
- status: typeof reason === "string" ? "rejected" : "approved",
1851
- actor: e.actor,
1852
- at: e.timestamp,
1853
- note: typeof reason === "string" ? reason : typeof comment === "string" ? comment : void 0,
1854
- source: typeof meta.source === "string" ? meta.source : void 0
1855
- }
1856
- ];
1857
- });
1858
- } catch (err) {
1859
- console.error(
1860
- `[external-edit] failed to read verdicts for ${nestId}/${documentId}:`,
1861
- err
1862
- );
1863
- return [];
1864
- }
1865
- }
1866
- async function mirrorVersion(input) {
1867
- const { storage } = await engineCache.get(input.nestId);
1868
- const node = await storage.readDocument(input.documentId);
1869
- const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-EFZGGL3Y.js");
1870
- await upsertVersion2({
1871
- nestId: input.nestId,
1872
- nodeId: input.documentId,
1873
- version: input.version,
1874
- content: node.body || "",
1875
- author: input.actor,
1876
- status: "published",
1877
- tags: node.frontmatter.tags || [],
1878
- changeNote: input.note
1879
- });
1880
- await setApprovedVersion2(
1881
- input.nestId,
1882
- input.documentId,
1883
- input.version,
1884
- input.actor
1885
- );
1886
- }
1887
- async function approveExternalEdit(input) {
1888
- const { storage } = await engineCache.get(input.nestId);
1889
- let result;
1890
- try {
1891
- result = await approveSuggestion({
1892
- storage,
1893
- rbac: communityRbac,
1894
- documentId: input.documentId,
1895
- suggestionId: input.suggestionId,
1896
- actor: input.actor,
1897
- zone: "default",
1898
- comment: input.comment
1899
- });
1900
- } catch (err) {
1901
- console.error(
1902
- `[external-edit] approveSuggestion failed for ${input.nestId}/${input.documentId} suggestion=${input.suggestionId}:`,
1903
- err
1904
- );
1905
- throw err;
1906
- }
1907
- try {
1908
- await mirrorVersion({
1909
- nestId: input.nestId,
1910
- documentId: input.documentId,
1911
- version: result.versionEntry.version,
1912
- actor: input.actor,
1913
- note: input.comment ? `External edit approved: ${input.comment}` : "External edit approved"
1914
- });
1915
- } catch (err) {
1916
- console.error(
1917
- `[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
1918
- err
1919
- );
1920
- }
1921
- const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
1922
- return { ...result, historyRecorded };
1923
- }
1924
- async function rejectExternalEdit(input) {
1925
- const { storage } = await engineCache.get(input.nestId);
1926
- const result = await rejectSuggestion({
1927
- storage,
1928
- rbac: communityRbac,
1929
- documentId: input.documentId,
1930
- suggestionId: input.suggestionId,
1931
- actor: input.actor,
1932
- zone: "default",
1933
- reason: input.reason
1934
- });
1935
- const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
1936
- try {
1937
- const approved = await loadChainHead(storage, input.documentId);
1938
- if (approved) {
1939
- await storage.writeDocument(input.documentId, approved.content);
1940
- }
1941
- } catch (err) {
1942
- console.error(
1943
- `[external-edit] revert-on-reject failed for ${input.nestId}/${input.documentId}:`,
1944
- err
1945
- );
1946
- }
1947
- return { ...result, historyRecorded };
1948
- }
1949
- var scannerTimer = null;
1950
- async function scanAllNests() {
1951
- const db = getDb();
1952
- const rows = await db.all("SELECT id FROM nests");
1953
- await Promise.all(
1954
- rows.map(
1955
- ({ id }) => scanNestForDrift(id).catch(
1956
- (err) => console.error(`[external-edit] scan failed for nest ${id}:`, err)
1957
- )
1958
- )
1959
- );
1960
- }
1961
- function startDriftScanner(intervalMs = 3e4) {
1962
- if (scannerTimer) return;
1963
- scannerTimer = setInterval(() => {
1964
- scanAllNests().catch(
1965
- (err) => console.error("[external-edit] scanner tick failed:", err)
1966
- );
1967
- }, intervalMs);
1968
- scannerTimer.unref?.();
1969
- }
1970
-
1971
1711
  // src/nodes/service.ts
1972
1712
  async function userIdFromEmail(email) {
1973
1713
  const db = getDb();
@@ -1986,17 +1726,42 @@ function assertTitleLength(title) {
1986
1726
  );
1987
1727
  }
1988
1728
  }
1989
- function assertTitleSluggable(title) {
1990
- if (!slugify(title)) {
1729
+ function assertTitleReadable(title) {
1730
+ if (!/[\p{L}\p{N}]/u.test(title)) {
1991
1731
  throw new ValidationError(
1992
- "the document id is built from the title using a-z and 0-9, so the title needs at least one of those"
1732
+ "the title needs at least one letter or number \u2014 it is what search and wiki links read back"
1993
1733
  );
1994
1734
  }
1995
1735
  }
1996
- var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1736
+ var slugify = (s) => s.normalize("NFKD").replace(/\p{M}+/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1737
+ async function generatedSlug(storage, prefix) {
1738
+ for (let n = 1; n <= 1e3; n++) {
1739
+ const slug = `document-${n}`;
1740
+ try {
1741
+ await storage.readDocument(`${prefix}${slug}`);
1742
+ } catch (err) {
1743
+ if (err instanceof DocumentNotFoundError) return slug;
1744
+ throw err;
1745
+ }
1746
+ }
1747
+ throw new ValidationError(
1748
+ "too many generated document ids in this folder \u2014 give the document a title with Latin letters or digits, or file it elsewhere"
1749
+ );
1750
+ }
1997
1751
  function isIdUnderFolder(id, folder) {
1998
1752
  return id.startsWith(`nodes/${folder}/`) || id.startsWith(`${folder}/`);
1999
1753
  }
1754
+ function assertSafeFolder(folder) {
1755
+ if (folder.split(/[/\\]/).some((seg) => seg === "..")) {
1756
+ throw new ValidationError(
1757
+ `Invalid folder "${folder}": path traversal ("..") is not allowed.`
1758
+ );
1759
+ }
1760
+ }
1761
+ async function storagePrefix(nestId) {
1762
+ const { storage } = await engineCache.get(nestId);
1763
+ return await storage.detectLayout() === "structured" ? "nodes" : "";
1764
+ }
2000
1765
  function folderToSegments(folder) {
2001
1766
  const segments = folder.split("/").map(slugify).filter(Boolean);
2002
1767
  if (folder.trim() && segments.length === 0) {
@@ -2005,8 +1770,10 @@ function folderToSegments(folder) {
2005
1770
  );
2006
1771
  }
2007
1772
  if (segments[0] === "nodes") segments.shift();
2008
- if (segments.length > 8) {
2009
- throw new ValidationError("folder may nest at most 8 levels deep");
1773
+ if (segments.length > MAX_FOLDER_DEPTH) {
1774
+ throw new ValidationError(
1775
+ `folder may nest at most ${MAX_FOLDER_DEPTH} levels deep`
1776
+ );
2010
1777
  }
2011
1778
  if (segments.some((seg) => seg.length > 100)) {
2012
1779
  throw new ValidationError(
@@ -2042,7 +1809,7 @@ function toSafeError(err, context, message) {
2042
1809
  }
2043
1810
  function bodyOnly(nodeId, raw) {
2044
1811
  try {
2045
- return parseDocument2(`${nodeId}.md`, raw, nodeId).body ?? "";
1812
+ return parseDocument(`${nodeId}.md`, raw, nodeId).body ?? "";
2046
1813
  } catch {
2047
1814
  return raw;
2048
1815
  }
@@ -2127,6 +1894,15 @@ async function resolveAuthorship(nestId, nodes) {
2127
1894
  }
2128
1895
  return result;
2129
1896
  }
1897
+ var folderScopeSupported = null;
1898
+ function engineScopesFolders() {
1899
+ if (folderScopeSupported === null) {
1900
+ const op = engineApi.getOperation("context_list");
1901
+ const properties = op ? inputJsonSchema(op).properties ?? {} : {};
1902
+ folderScopeSupported = "folder" in properties;
1903
+ }
1904
+ return folderScopeSupported;
1905
+ }
2130
1906
  async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
2131
1907
  if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
2132
1908
  throw new ForbiddenError(
@@ -2168,29 +1944,49 @@ async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {})
2168
1944
  };
2169
1945
  }
2170
1946
  async function listNodesForCaller(nestId, userId, filters = {}) {
1947
+ if (filters.folder !== void 0) assertSafeFolder(filters.folder);
2171
1948
  const { versions: versionManager } = await engineCache.get(nestId);
2172
1949
  const userEmail = await resolveCallerEmail(userId);
2173
- const { documents } = await engineApi.run(
2174
- "context_list",
2175
- {
2176
- ...filters.type ? { type: filters.type } : {},
2177
- ...filters.tag ? { tag: filters.tag } : {},
2178
- ...filters.status ? { status: filters.status } : {},
2179
- // Unlike the CLI and mcp-server, a retired node stays in this listing:
2180
- // here it is an unpublished document its stewards may still need to act
2181
- // on, not one removed from retrieval. An imported vault carrying
2182
- // `status: rejected` registers exactly that way.
2183
- include_retired: true,
2184
- full: true
2185
- },
2186
- await opContext(nestId, userEmail)
2187
- );
2188
- const nodes = documents.map((d) => ({
1950
+ const frontmatterFilters = {
1951
+ ...filters.type ? { type: filters.type } : {},
1952
+ ...filters.tag ? { tag: filters.tag } : {},
1953
+ ...filters.status ? { status: filters.status } : {},
1954
+ includeRetired: true
1955
+ };
1956
+ let documents;
1957
+ if (filters.withContent) {
1958
+ const scopeInEngine = filters.folder !== void 0 && engineScopesFolders();
1959
+ const scopePrefix = scopeInEngine ? await storagePrefix(nestId) : "";
1960
+ ({ documents } = await engineApi.run(
1961
+ "context_list",
1962
+ {
1963
+ ...frontmatterFilters,
1964
+ include_retired: true,
1965
+ ...scopeInEngine ? {
1966
+ folder: [scopePrefix, filters.folder].filter(Boolean).join("/"),
1967
+ recursive: false
1968
+ } : {},
1969
+ full: true
1970
+ },
1971
+ await opContext(nestId, userEmail)
1972
+ ));
1973
+ } else {
1974
+ await ensureNodeIndex(nestId);
1975
+ const indexed = await readIndexedNodes(
1976
+ nestId,
1977
+ filters.folder !== void 0 ? { folder: filters.folder } : {}
1978
+ );
1979
+ documents = filterDocuments(indexed, frontmatterFilters);
1980
+ }
1981
+ const scoped = filters.folder === void 0 ? documents : documents.filter((d) => folderOfId(d.id) === filters.folder);
1982
+ const nodes = scoped.map((d) => ({
2189
1983
  id: d.id,
2190
1984
  filePath: "",
2191
1985
  rawContent: "",
2192
1986
  frontmatter: d.frontmatter,
2193
- body: d.body
1987
+ // Empty on the indexed path: no listing renders a body, and the
1988
+ // approved-only branch below serves its own from the version store.
1989
+ body: d.body ?? ""
2194
1990
  }));
2195
1991
  const publicReader = await isPublicReader(nestId, userId);
2196
1992
  const approvedOnly = publicReader || filters.approvedOnly === true;
@@ -2207,6 +2003,13 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
2207
2003
  accessible = accessible.filter((d) => approvedVersions.has(d.id));
2208
2004
  }
2209
2005
  if (filters.limit) accessible = accessible.slice(0, filters.limit);
2006
+ const [displayStatuses, driftedDocs] = approvedVersions ? [null, null] : await Promise.all([
2007
+ getDisplayStatuses(nestId),
2008
+ // From the DB, not a vault walk. The disk truth behind it is
2009
+ // reconciled by the index rebuild and kept current by the stage /
2010
+ // approve / reject paths.
2011
+ nodesWithSuggestions(nestId)
2012
+ ]);
2210
2013
  const authorship = publicReader ? null : await resolveAuthorship(
2211
2014
  nestId,
2212
2015
  accessible.map((d) => ({
@@ -2249,22 +2052,125 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
2249
2052
  return r;
2250
2053
  }
2251
2054
  let pending = null;
2252
- try {
2253
- pending = await getPendingChange(nestId, doc.id);
2254
- } catch {
2255
- pending = null;
2055
+ if (driftedDocs?.has(doc.id)) {
2056
+ try {
2057
+ pending = await getPendingChange(nestId, doc.id);
2058
+ } catch {
2059
+ pending = null;
2060
+ }
2256
2061
  }
2257
2062
  if (pending) {
2258
2063
  r.pendingChange = pending;
2259
2064
  r.status = "external_edit_pending";
2260
2065
  } else {
2261
- r.status = await getDisplayStatus(nestId, r.id);
2066
+ r.status = displayStatuses?.get(r.id) ?? "draft";
2262
2067
  }
2263
2068
  return r;
2264
2069
  })
2265
2070
  );
2266
2071
  return enriched;
2267
2072
  }
2073
+ function withSubtreeTotals(direct) {
2074
+ return direct.map(({ path, count }) => ({
2075
+ path,
2076
+ count,
2077
+ total: direct.reduce(
2078
+ (sum, f) => f.path === path || f.path.startsWith(`${path}/`) ? sum + f.count : sum,
2079
+ 0
2080
+ )
2081
+ })).sort((a, b) => a.path.localeCompare(b.path));
2082
+ }
2083
+ async function listNestFolders(nestId, opts = {}) {
2084
+ if (opts.folder !== void 0) assertSafeFolder(opts.folder);
2085
+ await ensureNodeIndex(nestId);
2086
+ const counts = /* @__PURE__ */ new Map();
2087
+ for (const { path, count } of await indexedFolderCounts(nestId)) {
2088
+ counts.set(path, count);
2089
+ let parent = path;
2090
+ while (parent.includes("/")) {
2091
+ parent = parent.slice(0, parent.lastIndexOf("/"));
2092
+ if (!counts.has(parent)) counts.set(parent, 0);
2093
+ }
2094
+ }
2095
+ const direct = [...counts].map(([path, count]) => ({ path, count }));
2096
+ const scope = opts.folder ?? "";
2097
+ const inScope = direct.filter((f) => {
2098
+ if (scope && !f.path.startsWith(`${scope}/`)) return false;
2099
+ if (opts.recursive === false) {
2100
+ const rel = scope ? f.path.slice(scope.length + 1) : f.path;
2101
+ return !rel.includes("/");
2102
+ }
2103
+ return true;
2104
+ });
2105
+ return withSubtreeTotals(inScope);
2106
+ }
2107
+ var RECENT_LIMIT = 3;
2108
+ async function nestCensus(nestId, userId) {
2109
+ const userEmail = await resolveCallerEmail(userId);
2110
+ await ensureNodeIndex(nestId);
2111
+ const documents = await readIndexedNodes(nestId);
2112
+ const visible = await filterAccessible(nestId, userId, userEmail, documents);
2113
+ const tags = {};
2114
+ const types = {};
2115
+ const direct = /* @__PURE__ */ new Map();
2116
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
2117
+ let editedLast7d = 0;
2118
+ for (const d of visible) {
2119
+ const fm = d.frontmatter;
2120
+ types[fm.type || "document"] = (types[fm.type || "document"] || 0) + 1;
2121
+ for (const t of fm.tags || []) tags[String(t)] = (tags[String(t)] || 0) + 1;
2122
+ const folder = folderOfId(d.id);
2123
+ if (folder) direct.set(folder, (direct.get(folder) || 0) + 1);
2124
+ const edited = Date.parse(String(fm.updated_at || ""));
2125
+ if (!Number.isNaN(edited) && edited >= cutoff) editedLast7d++;
2126
+ }
2127
+ const paths = /* @__PURE__ */ new Set();
2128
+ for (const folder of direct.keys()) {
2129
+ const segs = folder.split("/");
2130
+ for (let i = 1; i <= segs.length; i++) paths.add(segs.slice(0, i).join("/"));
2131
+ }
2132
+ const folders = withSubtreeTotals(
2133
+ [...paths].map((path) => ({ path, count: direct.get(path) ?? 0 }))
2134
+ );
2135
+ const statusMap = await getDisplayStatuses(nestId);
2136
+ const statuses = {};
2137
+ for (const d of visible) {
2138
+ const st = statusMap.get(d.id) ?? "draft";
2139
+ statuses[st] = (statuses[st] || 0) + 1;
2140
+ }
2141
+ const authorship = await resolveAuthorship(
2142
+ nestId,
2143
+ visible.map((d) => ({
2144
+ id: d.id,
2145
+ frontmatterAuthor: d.frontmatter.author ?? null
2146
+ }))
2147
+ );
2148
+ const authors = [
2149
+ ...new Set(
2150
+ [...authorship.values()].map((a) => a.author).filter((a) => !!a)
2151
+ )
2152
+ ].sort((a, b) => a.localeCompare(b));
2153
+ const recent = visible.filter((d) => d.frontmatter.updated_at).sort(
2154
+ (a, b) => String(b.frontmatter.updated_at).localeCompare(
2155
+ String(a.frontmatter.updated_at)
2156
+ )
2157
+ ).slice(0, RECENT_LIMIT).map((d) => ({
2158
+ id: d.id,
2159
+ title: String(d.frontmatter.title ?? ""),
2160
+ type: d.frontmatter.type || "document",
2161
+ updated_at: d.frontmatter.updated_at
2162
+ }));
2163
+ return {
2164
+ total: visible.length,
2165
+ folders,
2166
+ tags,
2167
+ types,
2168
+ statuses,
2169
+ authors,
2170
+ editedLast7d,
2171
+ recent
2172
+ };
2173
+ }
2268
2174
  async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
2269
2175
  return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
2270
2176
  }
@@ -2290,14 +2196,27 @@ async function autoPublishReason(nestId, userEmail, doc) {
2290
2196
  return await actorAutoPublishes(nestId, userEmail) ? "owner/admin self-approve" : null;
2291
2197
  }
2292
2198
  async function createNode(nestId, input, userEmail) {
2199
+ const generatedId = !input.id && !slugify(input.title);
2200
+ for (let attempt = 0; ; attempt++) {
2201
+ try {
2202
+ return await createNodeOnce(nestId, input, userEmail);
2203
+ } catch (err) {
2204
+ if (!generatedId || attempt >= 3 || !(err instanceof ConflictError)) {
2205
+ throw err;
2206
+ }
2207
+ }
2208
+ }
2209
+ }
2210
+ async function createNodeOnce(nestId, input, userEmail) {
2293
2211
  const { storage, versions: versionManager } = await engineCache.get(nestId);
2294
2212
  assertTitleLength(input.title);
2295
- if (!input.id) assertTitleSluggable(input.title);
2213
+ assertTitleReadable(input.title);
2296
2214
  const slug = slugify(input.title).slice(0, 100).replace(/-+$/, "");
2297
2215
  let id = input.id;
2298
2216
  if (!id) {
2299
2217
  const folderSegments = folderToSegments(input.folder ?? "");
2300
- id = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/${slug}` : `nodes/${slug}`;
2218
+ const prefix = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/` : "nodes/";
2219
+ id = `${prefix}${slug || await generatedSlug(storage, prefix)}`;
2301
2220
  }
2302
2221
  const tags = (input.tags || []).map(normalizeTag2);
2303
2222
  const requestedType = input.type || "document";
@@ -2558,7 +2477,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
2558
2477
  if (patch.title) {
2559
2478
  if (patch.title !== node.frontmatter.title) {
2560
2479
  assertTitleLength(patch.title);
2561
- assertTitleSluggable(patch.title);
2480
+ assertTitleReadable(patch.title);
2562
2481
  }
2563
2482
  node = {
2564
2483
  ...node,
@@ -2706,6 +2625,8 @@ var NODE_KEYED_TABLES = [
2706
2625
  ["annotation_threads", "node_id"],
2707
2626
  ["comments", "node_id"],
2708
2627
  ["node_tag_index", "node_id"],
2628
+ ["node_index", "node_id"],
2629
+ ["node_suggestions", "node_id"],
2709
2630
  ["watchers", "node_id"],
2710
2631
  ["schedules", "agent_node"],
2711
2632
  // A live capability token bound to one agent node — deleting the agent must
@@ -2784,8 +2705,8 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
2784
2705
  await storage.writeDocument(newId, serializeNodeSafe(moved));
2785
2706
  try {
2786
2707
  const nestPath = resolveNestPath(nestId);
2787
- const oldChain = join2(nestPath, dirname(nodeId), ".versions", leaf);
2788
- const newChain = join2(nestPath, dirname(newId), ".versions", leaf);
2708
+ const oldChain = join(nestPath, dirname(nodeId), ".versions", leaf);
2709
+ const newChain = join(nestPath, dirname(newId), ".versions", leaf);
2789
2710
  if (oldChain !== newChain) {
2790
2711
  await mkdir(dirname(newChain), { recursive: true });
2791
2712
  await rename(oldChain, newChain);
@@ -2818,6 +2739,11 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
2818
2739
  await storage.deleteDocument(oldDerived);
2819
2740
  } catch {
2820
2741
  }
2742
+ try {
2743
+ await storage.regenerateIndex();
2744
+ } catch (err) {
2745
+ console.error("[move] regenerateIndex failed", nestId, nodeId, newId, err);
2746
+ }
2821
2747
  await trackEvent("node.move", { nestId, nodeId, newId });
2822
2748
  return { node: moved, previousId: nodeId };
2823
2749
  });
@@ -2888,7 +2814,7 @@ async function deleteFolder(nestId, folder, userEmail) {
2888
2814
 
2889
2815
  // src/nests/unsynced-service.ts
2890
2816
  import { readdirSync, readFileSync, statSync } from "fs";
2891
- import { join as join3, relative, resolve } from "path";
2817
+ import { join as join2, relative, resolve } from "path";
2892
2818
  import { rm } from "fs/promises";
2893
2819
  var RESERVED = /* @__PURE__ */ new Set(["nests"]);
2894
2820
  function isNestStorageRoot(absPath) {
@@ -2905,7 +2831,7 @@ function scanMarkdown(dir) {
2905
2831
  }
2906
2832
  for (const e of entries) {
2907
2833
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2908
- const full = join3(dir, e.name);
2834
+ const full = join2(dir, e.name);
2909
2835
  if (e.isDirectory()) {
2910
2836
  const sub = scanMarkdown(full);
2911
2837
  count += sub.count;
@@ -2955,7 +2881,7 @@ function collectLeafFolders(rel, abs) {
2955
2881
  if (!e.isDirectory()) continue;
2956
2882
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2957
2883
  out.push(
2958
- ...collectLeafFolders(`${rel}/${e.name}`, join3(abs, e.name))
2884
+ ...collectLeafFolders(`${rel}/${e.name}`, join2(abs, e.name))
2959
2885
  );
2960
2886
  }
2961
2887
  return out;
@@ -2973,7 +2899,7 @@ function listUnsyncedFolders() {
2973
2899
  if (!e.isDirectory()) continue;
2974
2900
  if (e.name.startsWith(".")) continue;
2975
2901
  if (RESERVED.has(e.name)) continue;
2976
- const abs = join3(root, e.name);
2902
+ const abs = join2(root, e.name);
2977
2903
  if (isNestStorageRoot(abs)) continue;
2978
2904
  out.push(...collectLeafFolders(e.name, abs));
2979
2905
  }
@@ -3010,7 +2936,7 @@ function collectMarkdownFiles(dir, root) {
3010
2936
  }
3011
2937
  for (const e of entries) {
3012
2938
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
3013
- const full = join3(dir, e.name);
2939
+ const full = join2(dir, e.name);
3014
2940
  if (e.isDirectory()) {
3015
2941
  out.push(...collectMarkdownFiles(full, root));
3016
2942
  } else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
@@ -3028,7 +2954,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
3028
2954
  `[unsynced] sync requested folder="${folderName}" user="${callerEmail}"`
3029
2955
  );
3030
2956
  assertSafeFolderName(folderName);
3031
- const src = join3(config.DATA_ROOT, folderName);
2957
+ const src = join2(config.DATA_ROOT, folderName);
3032
2958
  if (isNestStorageRoot(src)) {
3033
2959
  throw new ValidationError("Folder is not eligible for sync");
3034
2960
  }
@@ -3084,7 +3010,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
3084
3010
  async function deleteUnsyncedFolder(folderName) {
3085
3011
  console.log(`[unsynced] delete requested folder="${folderName}"`);
3086
3012
  assertSafeFolderName(folderName);
3087
- const src = join3(config.DATA_ROOT, folderName);
3013
+ const src = join2(config.DATA_ROOT, folderName);
3088
3014
  if (isNestStorageRoot(src)) {
3089
3015
  throw new ValidationError("Folder is not eligible for delete");
3090
3016
  }
@@ -3180,9 +3106,11 @@ nestRoutes.get("/", async (c) => {
3180
3106
  }
3181
3107
  const out = await Promise.all(deduped.map(annotate));
3182
3108
  const docRows = await db.all(
3183
- "SELECT nest_id, COUNT(DISTINCT node_id) AS c FROM node_versions GROUP BY nest_id"
3109
+ `SELECT nest_id, COUNT(DISTINCT node_id) AS c, MAX(created_at) AS last_activity
3110
+ FROM node_versions GROUP BY nest_id`
3184
3111
  );
3185
3112
  const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
3113
+ const activityByNest = new Map(docRows.map((r) => [r.nest_id, r.last_activity]));
3186
3114
  const collabRows = await db.all(
3187
3115
  "SELECT nest_id, COUNT(*) AS c FROM nest_collaborators GROUP BY nest_id"
3188
3116
  );
@@ -3236,6 +3164,7 @@ nestRoutes.get("/", async (c) => {
3236
3164
  collaborator_emails: []
3237
3165
  } : {
3238
3166
  ...n,
3167
+ last_activity: activityByNest.get(n.id) ?? null,
3239
3168
  document_count: docByNest.get(n.id) ?? 0,
3240
3169
  collaborator_count: collabByNest.get(n.id) ?? 0,
3241
3170
  steward_count: stewardByNest.get(n.id) ?? 0,
@@ -3499,7 +3428,7 @@ async function addCollaborator(params) {
3499
3428
  if (existing) {
3500
3429
  userId = existing.id;
3501
3430
  } else {
3502
- const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
3431
+ const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
3503
3432
  userId = uuid3();
3504
3433
  await db.run(
3505
3434
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
@@ -3691,7 +3620,7 @@ async function resolveGrantee(email) {
3691
3620
  const e = normalizeEmail(email);
3692
3621
  const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
3693
3622
  if (existing) return existing.id;
3694
- const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
3623
+ const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
3695
3624
  const id = uuid4();
3696
3625
  await db.run(
3697
3626
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
@@ -3699,6 +3628,41 @@ async function resolveGrantee(email) {
3699
3628
  );
3700
3629
  return id;
3701
3630
  }
3631
+ async function assertGrantAddsAccess(params) {
3632
+ const { nestId, email, userId, targetType, target, role } = params;
3633
+ const roles = [];
3634
+ if (userId) {
3635
+ const access = await resolveNestAccess(nestId, userId);
3636
+ if (access.ownerId === userId) roles.push("owner");
3637
+ const collab = collabPermToRole(access.collaboratorPermission);
3638
+ if (collab) roles.push(collab);
3639
+ for (const teamRole of await resolveTeamRolesForUser(nestId, userId)) {
3640
+ roles.push(teamRole);
3641
+ }
3642
+ }
3643
+ if (targetType === "document") {
3644
+ const stewards = await resolveStewardsForNode(nestId, target);
3645
+ for (const s of stewards) {
3646
+ if (s.steward.userEmail.toLowerCase() === email.toLowerCase()) {
3647
+ roles.push(s.steward.role);
3648
+ }
3649
+ }
3650
+ }
3651
+ const covered = role === "write" ? canEditWith(roles) : roles.length > 0;
3652
+ if (covered) {
3653
+ const held = primaryRole(roles) ?? "existing";
3654
+ throw new ValidationError(
3655
+ `${email} already has ${held} access here, so this ${targetType} is already ${role === "write" ? "editable by" : "visible to"} them.`
3656
+ );
3657
+ }
3658
+ if (!userId) return;
3659
+ const existing = await resolveNodeGrant(nestId, userId, target);
3660
+ if (existing && (role === "read" || existing === "write")) {
3661
+ throw new ValidationError(
3662
+ `${email} has already been shared this ${targetType} (${existing === "write" ? "can edit" : "can view"}).`
3663
+ );
3664
+ }
3665
+ }
3702
3666
  grantRoutes.post("/", async (c) => {
3703
3667
  const nestId = c.req.param("nestId");
3704
3668
  await requireManage(c, nestId);
@@ -3714,7 +3678,19 @@ grantRoutes.post("/", async (c) => {
3714
3678
  if (!["read", "write"].includes(role)) {
3715
3679
  throw new ValidationError("role must be read | write");
3716
3680
  }
3717
- const userId = await resolveGrantee(body.email);
3681
+ const email = normalizeEmail(body.email);
3682
+ const known = await getDb().get("SELECT id FROM users WHERE LOWER(email) = ?", [
3683
+ email
3684
+ ]);
3685
+ await assertGrantAddsAccess({
3686
+ nestId,
3687
+ email,
3688
+ userId: known?.id ?? null,
3689
+ targetType,
3690
+ target,
3691
+ role
3692
+ });
3693
+ const userId = known?.id ?? await resolveGrantee(body.email);
3718
3694
  const grant = await createGrant({
3719
3695
  nestId,
3720
3696
  targetType,
@@ -4122,7 +4098,7 @@ function toSearchHit(node, query) {
4122
4098
  import { Hono as Hono8 } from "hono";
4123
4099
 
4124
4100
  // src/nodes/markdown-export.ts
4125
- import { parseDocument as parseDocument3 } from "@promptowl/contextnest-engine";
4101
+ import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
4126
4102
  function nodeToMarkdown(node) {
4127
4103
  const tags = node.tags ?? [];
4128
4104
  return [
@@ -4158,7 +4134,7 @@ function rawContentType(nodeType) {
4158
4134
  function frontmatterBlockOf(raw) {
4159
4135
  if (!raw || !raw.startsWith("---")) return "";
4160
4136
  try {
4161
- const body = parseDocument3("frontmatter-split.md", raw, "frontmatter-split").body ?? "";
4137
+ const body = parseDocument2("frontmatter-split.md", raw, "frontmatter-split").body ?? "";
4162
4138
  if (raw.endsWith(body)) return raw.slice(0, raw.length - body.length);
4163
4139
  } catch {
4164
4140
  }
@@ -4204,7 +4180,14 @@ nodeRoutes.get("/", async (c) => {
4204
4180
  // Lets an owner preview what making the nest public would expose.
4205
4181
  ...q.approved_only === "1" || q.approved_only === "true" ? { approvedOnly: true } : {},
4206
4182
  // Ignore junk rather than 400 — a bad ?limit should not fail a listing.
4207
- ...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {}
4183
+ ...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {},
4184
+ // Present (even as "") = one folder level only, for the lazily-expanded
4185
+ // document tree. Absent = the whole nest, as every existing caller expects.
4186
+ ...q.folder !== void 0 ? { folder: q.folder } : {},
4187
+ // Bodies are off by default — the list is served from the DB index, which
4188
+ // holds metadata only. Opt back in and the listing crawls the vault for
4189
+ // them; kept for any client that was reading `content` off this route.
4190
+ ...q.content === "1" || q.content === "true" ? { withContent: true } : {}
4208
4191
  });
4209
4192
  return c.json({ count: nodes.length, nodes });
4210
4193
  });
@@ -4246,7 +4229,7 @@ nodeRoutes.post("/", async (c) => {
4246
4229
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4247
4230
  const nestId = c.req.param("nestId");
4248
4231
  const nodeId = c.req.param("nodeId");
4249
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-OHTTZFND.js");
4232
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-U4CKL7Y5.js");
4250
4233
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
4251
4234
  nestId,
4252
4235
  nodeId
@@ -4267,7 +4250,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4267
4250
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
4268
4251
  const nestId = c.req.param("nestId");
4269
4252
  const nodeId = c.req.param("nodeId");
4270
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-3CCKDO5N.js");
4253
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-H2M5ACPQ.js");
4271
4254
  const history = await getReviewHistory2(nestId, nodeId);
4272
4255
  return c.json({ reviews: history });
4273
4256
  });
@@ -5327,7 +5310,7 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
5327
5310
 
5328
5311
  // src/nests/bundle-service.ts
5329
5312
  import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
5330
- import { join as join4, sep } from "path";
5313
+ import { join as join3, sep } from "path";
5331
5314
  import JSZip from "jszip";
5332
5315
  import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
5333
5316
  function walkFiles(root) {
@@ -5340,7 +5323,7 @@ function walkFiles(root) {
5340
5323
  }
5341
5324
  for (const rel of entries) {
5342
5325
  try {
5343
- if (statSync2(join4(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
5326
+ if (statSync2(join3(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
5344
5327
  } catch {
5345
5328
  }
5346
5329
  }
@@ -5381,7 +5364,7 @@ async function exportNestBundle(nestId, includeDrafts) {
5381
5364
  const segs = rel.split("/");
5382
5365
  const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
5383
5366
  if (!keep) continue;
5384
- zip.file(rel, readFileSync2(join4(nestPath, rel)));
5367
+ zip.file(rel, readFileSync2(join3(nestPath, rel)));
5385
5368
  }
5386
5369
  trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
5387
5370
  return zip.generateAsync({ type: "arraybuffer" });
@@ -5552,7 +5535,9 @@ queryRoutes.get("/changes", async (c) => {
5552
5535
  const includeContent = ["1", "true"].includes(
5553
5536
  c.req.query("include_content") || ""
5554
5537
  );
5555
- const all = await listNodesForCaller(nestId, userId);
5538
+ const all = await listNodesForCaller(nestId, userId, {
5539
+ ...includeContent ? { withContent: true } : {}
5540
+ });
5556
5541
  const db = getDb();
5557
5542
  const publicReader = await isPublicReader(nestId, userId);
5558
5543
  const approvedRows = await db.all(
@@ -5619,7 +5604,10 @@ queryRoutes.get("/runnables", async (c) => {
5619
5604
  const nestId = c.req.param("nestId");
5620
5605
  const userId = c.get("userId");
5621
5606
  const all = await listNodesForCaller(nestId, userId, {
5622
- type: RUNNABLE_NODE_TYPES
5607
+ type: RUNNABLE_NODE_TYPES,
5608
+ // The prompt/skill body IS the payload here — an orchestrator pulls it to
5609
+ // run the thing, so this surface pays for the file reads.
5610
+ withContent: true
5623
5611
  });
5624
5612
  const runnables = all.map((n) => ({
5625
5613
  id: n.id,
@@ -5914,6 +5902,25 @@ queryRoutes.get("/overview", async (c) => {
5914
5902
  }))
5915
5903
  });
5916
5904
  });
5905
+ queryRoutes.get("/census", async (c) => {
5906
+ const census = await nestCensus(c.req.param("nestId"), c.get("userId"));
5907
+ return c.json({ census });
5908
+ });
5909
+ queryRoutes.get("/folders", async (c) => {
5910
+ const q = c.req.query();
5911
+ const folders = await listNestFolders(c.req.param("nestId"), {
5912
+ ...q.folder !== void 0 ? { folder: q.folder } : {},
5913
+ ...q.recursive === "0" || q.recursive === "false" ? { recursive: false } : {}
5914
+ });
5915
+ return c.json({ folders });
5916
+ });
5917
+ queryRoutes.post("/reindex", async (c) => {
5918
+ if (permissionLevel(c.get("nestPermission")) < permissionLevel("write")) {
5919
+ throw new ForbiddenError("Reindexing a nest requires write access");
5920
+ }
5921
+ const indexed = await rebuildNodeIndex(c.req.param("nestId"));
5922
+ return c.json({ indexed });
5923
+ });
5917
5924
  queryRoutes.get("/comment-counts", async (c) => {
5918
5925
  const counts = await countThreadsByNode(c.req.param("nestId"));
5919
5926
  return c.json({ counts });
@@ -6058,30 +6065,6 @@ import { Hono as Hono12 } from "hono";
6058
6065
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6059
6066
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6060
6067
 
6061
- // src/shared/node-id.ts
6062
- function assertSafeNodeId(rawId) {
6063
- let id = rawId;
6064
- try {
6065
- id = decodeURIComponent(rawId);
6066
- } catch {
6067
- }
6068
- if (!id || id.length > 512) {
6069
- throw new ValidationError("invalid node id");
6070
- }
6071
- if (id.includes("\0") || id.includes("\\")) {
6072
- throw new ValidationError("invalid node id");
6073
- }
6074
- if (id.startsWith("/")) {
6075
- throw new ValidationError("invalid node id (absolute path)");
6076
- }
6077
- for (const seg of id.split("/")) {
6078
- if (seg === "" || seg === "." || seg === "..") {
6079
- throw new ValidationError("invalid node id (path traversal)");
6080
- }
6081
- }
6082
- return id;
6083
- }
6084
-
6085
6068
  // src/telemetry/trace-log.ts
6086
6069
  var PRUNE_EVERY = 500;
6087
6070
  var insertsSincePrune = 0;
@@ -6385,6 +6368,30 @@ async function appendRunStep(runId, input) {
6385
6368
  return { seq };
6386
6369
  }
6387
6370
  var RUN_STATUSES = ["running", "succeeded", "failed", "cancelled"];
6371
+ async function cancelDescendants(runId) {
6372
+ const db = getDb();
6373
+ const at = (/* @__PURE__ */ new Date()).toISOString();
6374
+ const trace = JSON.stringify({ error: "Parent run cancelled" });
6375
+ let frontier = [runId];
6376
+ let cancelled = 0;
6377
+ for (let depth = 0; frontier.length && depth < 64; depth++) {
6378
+ const ph = frontier.map(() => "?").join(",");
6379
+ const kids = await db.all(
6380
+ `SELECT id FROM runs WHERE parent_run_id IN (${ph}) AND status = 'running'`,
6381
+ frontier
6382
+ );
6383
+ if (!kids.length) break;
6384
+ frontier = kids.map((k) => k.id);
6385
+ const kph = frontier.map(() => "?").join(",");
6386
+ await db.run(
6387
+ `UPDATE runs SET status = 'cancelled', trace = COALESCE(trace, ?), finished_at = ?
6388
+ WHERE id IN (${kph}) AND status = 'running'`,
6389
+ [trace, at, ...frontier]
6390
+ );
6391
+ cancelled += frontier.length;
6392
+ }
6393
+ return cancelled;
6394
+ }
6388
6395
  async function closeRun(runId, input) {
6389
6396
  const status = input.status;
6390
6397
  if (typeof status !== "string" || !RUN_STATUSES.includes(status)) {
@@ -6406,6 +6413,7 @@ async function closeRun(runId, input) {
6406
6413
  if (!run) throw new NotFoundError("Run not found");
6407
6414
  throw new ConflictError("Run already terminal \u2014 status can't be changed");
6408
6415
  }
6416
+ if (status === "cancelled") await cancelDescendants(runId);
6409
6417
  return { status };
6410
6418
  }
6411
6419
  async function getRunTrace(run, opts) {
@@ -7709,7 +7717,9 @@ ${context_md}`;
7709
7717
  {},
7710
7718
  await opContext(nestId, userEmail)
7711
7719
  );
7712
- const visible = await listNodesForCallerByEmail(nestId, userEmail, {});
7720
+ const visible = await listNodesForCallerByEmail(nestId, userEmail, {
7721
+ withContent: true
7722
+ });
7713
7723
  const types = {};
7714
7724
  const tags = {};
7715
7725
  for (const n of visible) {
@@ -8544,14 +8554,14 @@ ${list}`,
8544
8554
  if (!target) return "target is required (a node id or folder prefix).";
8545
8555
  if (!["read", "write"].includes(role)) return "role must be read or write.";
8546
8556
  try {
8547
- const { createGrant: createGrant2 } = await import("./grants-service-4NXPBYWD.js");
8548
- const db = (await import("./client-OBS5HOTA.js")).getDb();
8557
+ const { createGrant: createGrant2 } = await import("./grants-service-ME7HIVHU.js");
8558
+ const db = (await import("./client-SW5I6ZBK.js")).getDb();
8549
8559
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
8550
8560
  const e = normalizeEmail2(String(args.email || ""));
8551
8561
  if (!e) return "email is required.";
8552
8562
  let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
8553
8563
  if (!row) {
8554
- const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
8564
+ const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
8555
8565
  const { v4: uuid16 } = await import("uuid");
8556
8566
  const id = uuid16();
8557
8567
  await db.run(
@@ -9014,8 +9024,9 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
9014
9024
  throw new ValidationError("status must be running | succeeded | failed | cancelled");
9015
9025
  }
9016
9026
  const terminal = status !== "running";
9017
- await getDb().run(
9018
- `UPDATE runs SET status = ?, trace = COALESCE(?, trace), finished_at = ? WHERE id = ?`,
9027
+ const res = await getDb().run(
9028
+ `UPDATE runs SET status = ?, trace = COALESCE(?, trace), finished_at = ?
9029
+ WHERE id = ? AND status = 'running'`,
9019
9030
  [
9020
9031
  status,
9021
9032
  body.trace === void 0 ? null : JSON.stringify(body.trace),
@@ -9023,6 +9034,13 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
9023
9034
  run.id
9024
9035
  ]
9025
9036
  );
9037
+ if (!res.changes) {
9038
+ const now = await getDb().get("SELECT status FROM runs WHERE id = ?", [
9039
+ run.id
9040
+ ]);
9041
+ throw new ConflictError(`Run already terminal (${now?.status ?? "closed"})`);
9042
+ }
9043
+ if (status === "cancelled") await cancelDescendants(run.id);
9026
9044
  if (status === "failed") {
9027
9045
  const err = body.trace && typeof body.trace === "object" ? String(body.trace.error ?? "").slice(0, 200) : "";
9028
9046
  void dispatchEvent({
@@ -9631,8 +9649,8 @@ definitionRoutes.delete("/:id", async (c) => {
9631
9649
  // src/nests/asset-routes.ts
9632
9650
  import { Hono as Hono18 } from "hono";
9633
9651
  import { v4 as uuid13 } from "uuid";
9634
- import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
9635
- import { join as join5 } from "path";
9652
+ import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
9653
+ import { join as join4 } from "path";
9636
9654
  var assetRoutes = new Hono18();
9637
9655
  var CONTENT_TYPES = {
9638
9656
  png: "image/png",
@@ -9680,9 +9698,9 @@ assetRoutes.post("/", async (c) => {
9680
9698
  );
9681
9699
  }
9682
9700
  const name = `${uuid13()}.${ext === "jpeg" ? "jpg" : ext}`;
9683
- const dir = join5(resolveNestPath(nestId), "assets");
9701
+ const dir = join4(resolveNestPath(nestId), "assets");
9684
9702
  await mkdir2(dir, { recursive: true });
9685
- await writeFile(join5(dir, name), Buffer.from(await file.arrayBuffer()));
9703
+ await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
9686
9704
  const url = `/nests/${nestId}/assets/${name}`;
9687
9705
  return c.json(
9688
9706
  {
@@ -9704,7 +9722,7 @@ assetRoutes.get("/:file", async (c) => {
9704
9722
  }
9705
9723
  let bytes;
9706
9724
  try {
9707
- bytes = await readFile2(join5(resolveNestPath(nestId), "assets", name));
9725
+ bytes = await readFile(join4(resolveNestPath(nestId), "assets", name));
9708
9726
  } catch {
9709
9727
  throw new NotFoundError("Asset not found");
9710
9728
  }
@@ -10514,7 +10532,7 @@ function rowToComment(row) {
10514
10532
 
10515
10533
  // src/governance/stewards-parser.ts
10516
10534
  import { readFileSync as readFileSync3, existsSync } from "fs";
10517
- import { join as join6 } from "path";
10535
+ import { join as join5 } from "path";
10518
10536
  function parseStewardsYaml(content) {
10519
10537
  const result = { version: 1 };
10520
10538
  const lines = content.split("\n");
@@ -10580,9 +10598,9 @@ function parseEntry(str) {
10580
10598
  function loadStewardsConfig(nestId) {
10581
10599
  const nestPath = resolveNestPath(nestId);
10582
10600
  const candidates = [
10583
- join6(nestPath, "stewards.yaml"),
10584
- join6(nestPath, "stewards.yml"),
10585
- join6(nestPath, ".context", "stewards.yaml")
10601
+ join5(nestPath, "stewards.yaml"),
10602
+ join5(nestPath, "stewards.yml"),
10603
+ join5(nestPath, ".context", "stewards.yaml")
10586
10604
  ];
10587
10605
  for (const candidatePath of candidates) {
10588
10606
  if (existsSync(candidatePath)) {
@@ -10943,7 +10961,7 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
10943
10961
  const approved = await getApprovedVersion(nestId, nodeId);
10944
10962
  const { versions: versionManager } = await engineCache.get(nestId);
10945
10963
  const resolutions = await getDb().all(
10946
- `SELECT version, status, resolved_by
10964
+ `SELECT version, status, resolved_by, resolution_note
10947
10965
  FROM review_requests
10948
10966
  WHERE nest_id = ? AND node_id = ?
10949
10967
  AND status IN ('approved', 'rejected')
@@ -10954,13 +10972,21 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
10954
10972
  const byVersion = /* @__PURE__ */ new Map();
10955
10973
  for (const r of resolutions) {
10956
10974
  if (!byVersion.has(r.version)) {
10957
- byVersion.set(r.version, { status: r.status, resolvedBy: r.resolved_by });
10975
+ byVersion.set(r.version, {
10976
+ status: r.status,
10977
+ resolvedBy: r.resolved_by,
10978
+ resolutionNote: r.resolution_note?.trim() || void 0
10979
+ });
10958
10980
  }
10959
10981
  }
10960
10982
  const enriched = await Promise.all(
10961
10983
  allVersions.map(async (v) => {
10962
10984
  const r = byVersion.get(v.version);
10963
- const resolution = r ? { resolvedBy: r.resolvedBy, resolutionStatus: r.status } : {};
10985
+ const resolution = r ? {
10986
+ resolvedBy: r.resolvedBy,
10987
+ resolutionStatus: r.status,
10988
+ resolutionNote: r.resolutionNote
10989
+ } : {};
10964
10990
  try {
10965
10991
  const diff = await versionManager.getDiff(nodeId, v.version);
10966
10992
  return { ...v, ...resolution, diff: diff ?? void 0 };
@@ -11338,19 +11364,19 @@ async function ensureAnonymousUser() {
11338
11364
  // src/app.ts
11339
11365
  import { serveStatic } from "@hono/node-server/serve-static";
11340
11366
  import { fileURLToPath } from "url";
11341
- import { dirname as dirname2, join as join7, relative as relative2 } from "path";
11367
+ import { dirname as dirname2, join as join6, relative as relative2 } from "path";
11342
11368
  import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
11343
11369
  var HERE = dirname2(fileURLToPath(import.meta.url));
11344
11370
  var SERVICE_VERSION = (() => {
11345
11371
  try {
11346
- return JSON.parse(readFileSync4(join7(HERE, "..", "package.json"), "utf8")).version;
11372
+ return JSON.parse(readFileSync4(join6(HERE, "..", "package.json"), "utf8")).version;
11347
11373
  } catch {
11348
11374
  return "unknown";
11349
11375
  }
11350
11376
  })();
11351
11377
  var UI_DIR_CANDIDATES = [
11352
- join7(HERE, "web3"),
11353
- join7(process.cwd(), "dist", "web3")
11378
+ join6(HERE, "web3"),
11379
+ join6(process.cwd(), "dist", "web3")
11354
11380
  ];
11355
11381
  var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
11356
11382
  var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";