@promptowl/contextnest-community 1.20.0 → 1.21.1

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 (24) hide show
  1. package/README.md +1 -1
  2. package/dist/{adapter.postgres-YOODX2BI.js → adapter.postgres-6VZCMPQL.js} +11 -1
  3. package/dist/{chunk-KHMSDTJJ.js → chunk-FXYFLUE7.js} +60 -12
  4. package/dist/{chunk-7CPP6FIU.js → chunk-G4AL7ETF.js} +173 -34
  5. package/dist/{chunk-3LUUJUWS.js → chunk-IVXVOXYN.js} +6 -2
  6. package/dist/{chunk-O5DYL7ZG.js → chunk-LL2JU5AA.js} +2 -2
  7. package/dist/{chunk-GVUBZTI3.js → chunk-NH7L2S6S.js} +93 -40
  8. package/dist/{chunk-XCFKS65Y.js → chunk-SKZ7YYT4.js} +26 -2
  9. package/dist/{chunk-EY7WOKJR.js → chunk-UVOAYQTU.js} +12 -4
  10. package/dist/{client-7Z2IVCPW.js → client-KBK4VIWJ.js} +1 -1
  11. package/dist/{engine-VZTSDUPB.js → engine-WXWER2V7.js} +2 -2
  12. package/dist/{external-edit-service-3FVK3OD7.js → external-edit-service-Y757EFMS.js} +3 -3
  13. package/dist/{grants-service-WC27TRY3.js → grants-service-FKINSDLZ.js} +2 -2
  14. package/dist/index.js +551 -150
  15. package/dist/{migrations.postgres-L42OXR7K.js → migrations.postgres-JA2YGLS3.js} +20 -1
  16. package/dist/{review-service-TJRHRMMZ.js → review-service-PPR5BV5J.js} +8 -6
  17. package/dist/{stewardship-service-HRVD7SIS.js → stewardship-service-IKC7MXLP.js} +3 -3
  18. package/dist/{version-service-5PIFT2OY.js → version-service-GWSDSUZR.js} +11 -3
  19. package/dist/web3/assets/index-T4pilFNL.css +1 -0
  20. package/dist/web3/assets/index-deyhV93k.js +1382 -0
  21. package/dist/web3/index.html +2 -2
  22. package/package.json +1 -1
  23. package/dist/web3/assets/index-D4UG3T04.js +0 -1365
  24. package/dist/web3/assets/index-Dw1UTDIk.css +0 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@ ContextNest Community Edition is a self-hosted server that lets you:
15
15
  - Export a nest as a portable bundle and re-import it on another self-hosted host
16
16
  - Apply stewardship workflows — draft, pending review, approved
17
17
  - Share nests with collaborators or publish them read-only to the public
18
- - Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`
18
+ - Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`, or register the nest in the ctx CLI (`ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`)
19
19
  - Sync with the PromptOwl hosted platform for multi-user collaboration
20
20
 
21
21
  The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
@@ -5,7 +5,17 @@ function translatePlaceholders(sql) {
5
5
  let n = 0;
6
6
  for (let i = 0; i < sql.length; i++) {
7
7
  const ch = sql[i];
8
- if (ch === "'") {
8
+ if (!inStr && ch === "-" && sql[i + 1] === "-") {
9
+ const end = sql.indexOf("\n", i);
10
+ const stop = end === -1 ? sql.length : end;
11
+ out += sql.slice(i, stop);
12
+ i = stop - 1;
13
+ } else if (!inStr && ch === "/" && sql[i + 1] === "*") {
14
+ const end = sql.indexOf("*/", i + 2);
15
+ const stop = end === -1 ? sql.length : end + 2;
16
+ out += sql.slice(i, stop);
17
+ i = stop - 1;
18
+ } else if (ch === "'") {
9
19
  inStr = !inStr;
10
20
  out += ch;
11
21
  } else if (ch === "?" && !inStr) {
@@ -2,10 +2,10 @@ import {
2
2
  buildTitleMap,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-7CPP6FIU.js";
5
+ } from "./chunk-G4AL7ETF.js";
6
6
  import {
7
7
  getDb
8
- } from "./chunk-XCFKS65Y.js";
8
+ } from "./chunk-SKZ7YYT4.js";
9
9
 
10
10
  // src/governance/version-service.ts
11
11
  import { createHash } from "crypto";
@@ -136,6 +136,44 @@ async function setApprovedVersion(nestId, nodeId, version, approvedBy) {
136
136
  );
137
137
  await db.run(sql, [nestId, nodeId, version, approvedBy]);
138
138
  }
139
+ async function getSealedVersion(nestId, nodeId) {
140
+ const approved = await getApprovedVersion(nestId, nodeId);
141
+ if (approved != null) return approved;
142
+ const db = getDb();
143
+ const row = await db.get(
144
+ `SELECT MAX(version) as v FROM node_versions
145
+ WHERE nest_id = ? AND node_id = ? AND status IN ('published', 'approved')`,
146
+ [nestId, nodeId]
147
+ );
148
+ return row?.v ?? null;
149
+ }
150
+ async function deleteSupersededDrafts(nestId, nodeId, belowVersion) {
151
+ const db = getDb();
152
+ await db.run(
153
+ `DELETE FROM node_versions
154
+ WHERE nest_id = ? AND node_id = ? AND status = 'draft' AND version < ?`,
155
+ [nestId, nodeId, belowVersion]
156
+ );
157
+ }
158
+ async function deleteUnsealedVersions(nestId, nodeId, sealedVersion) {
159
+ const db = getDb();
160
+ await db.run(
161
+ `DELETE FROM node_versions
162
+ WHERE nest_id = ? AND node_id = ? AND version > ?
163
+ AND status IN ('draft', 'rejected', 'pending_review')`,
164
+ [nestId, nodeId, sealedVersion]
165
+ );
166
+ }
167
+ async function hasUnsealedDraft(nestId, nodeId) {
168
+ const db = getDb();
169
+ const row = await db.get(
170
+ `SELECT status FROM node_versions
171
+ WHERE nest_id = ? AND node_id = ?
172
+ ORDER BY version DESC LIMIT 1`,
173
+ [nestId, nodeId]
174
+ );
175
+ return row?.status === "draft" || row?.status === "rejected" || row?.status === "pending_review";
176
+ }
139
177
  async function checkConflict(nestId, nodeId, baseVersion) {
140
178
  const db = getDb();
141
179
  const current = await db.get(
@@ -265,20 +303,16 @@ async function listMyDrafts(params) {
265
303
  WHERE r.nest_id = v.nest_id AND r.node_id = v.node_id
266
304
  AND r.status = 'pending')`;
267
305
  const args = [params.authorEmail.toLowerCase(), ...params.nestIds];
268
- const total = Number(
269
- (await db.get(
270
- `SELECT COUNT(*) as c FROM node_versions v ${where}`,
271
- args
272
- )).c
273
- );
274
306
  const limit = Math.min(Math.max(params.limit ?? 25, 1), 100);
275
307
  const offset = Math.max(params.offset ?? 0, 0);
308
+ const q = params.q?.trim().toLowerCase() || "";
309
+ const SCAN_CAP = 500;
276
310
  const rows = await db.all(
277
311
  `SELECT v.nest_id, v.node_id, v.status, v.version, v.created_at
278
312
  FROM node_versions v ${where}
279
313
  ORDER BY v.created_at DESC
280
- LIMIT ? OFFSET ?`,
281
- [...args, limit, offset]
314
+ LIMIT ?`,
315
+ [...args, SCAN_CAP]
282
316
  );
283
317
  const titlesByNest = new Map(
284
318
  await Promise.all(
@@ -290,7 +324,7 @@ async function listMyDrafts(params) {
290
324
  )
291
325
  )
292
326
  );
293
- const drafts = rows.filter((r) => titlesByNest.get(r.nest_id)?.has(r.node_id)).map((r) => ({
327
+ let drafts = rows.filter((r) => titlesByNest.get(r.nest_id)?.has(r.node_id)).map((r) => ({
294
328
  nestId: r.nest_id,
295
329
  nodeId: r.node_id,
296
330
  title: titlesByNest.get(r.nest_id).get(r.node_id) || r.node_id,
@@ -300,7 +334,17 @@ async function listMyDrafts(params) {
300
334
  version: Number(r.version),
301
335
  updatedAt: r.created_at
302
336
  }));
303
- return { drafts, total };
337
+ if (q) {
338
+ drafts = drafts.filter(
339
+ (d) => [d.title, params.nestNames?.get(d.nestId) ?? "", d.nodeId].some(
340
+ (s) => s.toLowerCase().includes(q)
341
+ )
342
+ );
343
+ }
344
+ if (params.sort === "title") {
345
+ drafts.sort((a, b) => a.title.localeCompare(b.title));
346
+ }
347
+ return { drafts: drafts.slice(offset, offset + limit), total: drafts.length };
304
348
  }
305
349
  function rowToVersion(row) {
306
350
  return {
@@ -327,6 +371,10 @@ export {
327
371
  getApprovedVersion,
328
372
  getApprovedVersions,
329
373
  setApprovedVersion,
374
+ getSealedVersion,
375
+ deleteSupersededDrafts,
376
+ deleteUnsealedVersions,
377
+ hasUnsealedDraft,
330
378
  checkConflict,
331
379
  getNodeTags,
332
380
  getDisplayStatus,
@@ -8,7 +8,7 @@ import {
8
8
  config,
9
9
  getDb,
10
10
  isEmailish
11
- } from "./chunk-XCFKS65Y.js";
11
+ } from "./chunk-SKZ7YYT4.js";
12
12
  import {
13
13
  ANON_USER_ID
14
14
  } from "./chunk-YB3LKF7U.js";
@@ -680,6 +680,14 @@ var STATUS_META = {
680
680
  subjectWord: "deleted",
681
681
  label: "Deleted",
682
682
  color: "#dc2626"
683
+ },
684
+ // An honoured ARCHIVE request. Deliberately not the deletion palette: the
685
+ // nest is intact and restorable, and red "Deleted" would tell the requester
686
+ // the opposite of what happened.
687
+ deletion_archived: {
688
+ subjectWord: "archived",
689
+ label: "Archived",
690
+ color: "#0891b2"
683
691
  }
684
692
  };
685
693
  var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
@@ -777,6 +785,14 @@ var TEMPLATES = {
777
785
  lineH: (x) => `${x.doc} has been deleted${x.byH}${x.noteH} \u2014 your deletion request was accepted.`,
778
786
  rows: governanceRows
779
787
  },
788
+ deletion_archived: {
789
+ heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
790
+ detailsHeader: "Nest details",
791
+ linkLabel: "Open nest",
792
+ lineT: (x) => `${x.title} has been archived${x.byT}${x.noteT} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
793
+ lineH: (x) => `${x.doc} has been archived${x.byH}${x.noteH} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
794
+ rows: governanceRows
795
+ },
780
796
  steward: {
781
797
  heading: (x) => `You're now a steward of ${x.title}`,
782
798
  detailsHeader: "Details",
@@ -1259,7 +1275,9 @@ async function listNotifications(userEmail, opts = {}) {
1259
1275
  `SELECT n.id, n.nest_id, n.kind, n.subject_id, n.message, n.created_at, n.read_at,
1260
1276
  COALESCE(
1261
1277
  rr.node_id,
1262
- CASE WHEN dr.target_type = 'document' THEN dr.node_id END
1278
+ CASE WHEN dr.target_type = 'document' THEN dr.node_id END,
1279
+ -- A mention names the document directly; there's no request row.
1280
+ CASE WHEN n.kind = 'mention' THEN n.subject_id END
1263
1281
  ) AS node_id
1264
1282
  FROM notifications n
1265
1283
  LEFT JOIN review_requests rr ON rr.id = n.subject_id
@@ -1566,6 +1584,23 @@ function parseSharedTeams(json) {
1566
1584
  return [];
1567
1585
  }
1568
1586
  }
1587
+ async function listSharedTeamMembers(refs) {
1588
+ const ids = [...new Set(refs.map((r) => r.teamId).filter(Boolean))];
1589
+ if (ids.length === 0) return [];
1590
+ const rows = await getDb().all(
1591
+ `SELECT members FROM teams WHERE id IN (${ids.map(() => "?").join(",")})`,
1592
+ ids
1593
+ );
1594
+ const byEmail = /* @__PURE__ */ new Map();
1595
+ for (const row of rows) {
1596
+ for (const member of parseMembers(row.members)) {
1597
+ const key = (member.email || "").toLowerCase();
1598
+ if (!key || byEmail.has(key)) continue;
1599
+ byEmail.set(key, { userId: member.userId, email: member.email, name: null });
1600
+ }
1601
+ }
1602
+ return [...byEmail.values()];
1603
+ }
1569
1604
  async function listNestTeams(nestId) {
1570
1605
  const db = getDb();
1571
1606
  const row = await db.get("SELECT shared_teams FROM nests WHERE id = ?", [
@@ -1750,9 +1785,29 @@ var PERMISSION_LEVELS = {
1750
1785
  };
1751
1786
  var higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1752
1787
  async function resolveNestAccess(nestId, userId) {
1788
+ const access = await resolveNestAccessRaw(nestId, userId);
1789
+ return {
1790
+ ...access,
1791
+ rawPermission: access.permission,
1792
+ permission: capForArchive(access.permission, access.archived)
1793
+ };
1794
+ }
1795
+ async function isNestArchived(nestId) {
1796
+ const row = await getDb().get(
1797
+ "SELECT archived_at FROM nests WHERE id = ?",
1798
+ [nestId]
1799
+ );
1800
+ return !!row?.archived_at;
1801
+ }
1802
+ function capForArchive(permission, archived) {
1803
+ if (!archived) return permission;
1804
+ return PERMISSION_LEVELS[permission] > PERMISSION_LEVELS.read ? "read" : permission;
1805
+ }
1806
+ async function resolveNestAccessRaw(nestId, userId) {
1753
1807
  const row = await getDb().get(
1754
1808
  `SELECT n.user_id AS owner_id,
1755
1809
  n.visibility AS visibility,
1810
+ n.archived_at AS archived_at,
1756
1811
  n.shared_teams AS shared_teams,
1757
1812
  u.email AS caller_email,
1758
1813
  nc.permission AS collab_permission,
@@ -1779,7 +1834,9 @@ async function resolveNestAccess(nestId, userId) {
1779
1834
  isSteward: false,
1780
1835
  hasGrant: false,
1781
1836
  permission: "none",
1782
- isPublicReader: false
1837
+ rawPermission: "none",
1838
+ isPublicReader: false,
1839
+ archived: false
1783
1840
  };
1784
1841
  if (!row) return absent;
1785
1842
  const base = {
@@ -1791,7 +1848,8 @@ async function resolveNestAccess(nestId, userId) {
1791
1848
  // Postgres returns integer 1, SQLite returns 1 — both truthy, neither a
1792
1849
  // boolean, so normalize rather than leaking the driver's shape.
1793
1850
  isSteward: !!row.steward_hit,
1794
- hasGrant: !!row.grant_hit
1851
+ hasGrant: !!row.grant_hit,
1852
+ archived: !!row.archived_at
1795
1853
  };
1796
1854
  if (row.owner_id === userId) {
1797
1855
  return { ...base, permission: "owner", isPublicReader: false };
@@ -1877,20 +1935,32 @@ async function isImportedNest(nestId) {
1877
1935
  );
1878
1936
  return !!row?.is_imported;
1879
1937
  }
1938
+ var MAX_NEST_NAME_LENGTH = 100;
1880
1939
  function plainName(name, emptyMessage = "Name cannot be empty") {
1881
1940
  const typed = (name ?? "").trim();
1882
1941
  if (!typed) throw new ValidationError(emptyMessage);
1883
- const stripped = typed.replace(/<[^>]*>/g, "").trim();
1884
- if (!stripped) {
1942
+ if (typed.length > MAX_NEST_NAME_LENGTH) {
1943
+ throw new ValidationError(
1944
+ `Nest names are capped at ${MAX_NEST_NAME_LENGTH} characters`
1945
+ );
1946
+ }
1947
+ if (/<\/?[a-zA-Z][^>]*>/.test(typed)) {
1885
1948
  throw new ValidationError(
1886
- "That's not a valid nest name \u2014 it's only HTML markup. Use plain text."
1949
+ "Nest names are plain text \u2014 remove the HTML tags."
1887
1950
  );
1888
1951
  }
1889
- return stripped;
1952
+ return typed;
1890
1953
  }
1891
1954
  function toSlug(name) {
1892
1955
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1893
1956
  }
1957
+ async function assertSlugFree(userId, slug, message, exceptId) {
1958
+ const conflict = await getDb().get(
1959
+ exceptId ? "SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1" : "SELECT id FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
1960
+ exceptId ? [userId, slug, exceptId] : [userId, slug]
1961
+ );
1962
+ if (conflict) throw new ValidationError(message);
1963
+ }
1894
1964
  async function uniqueNestName(userId, baseName) {
1895
1965
  const db = getDb();
1896
1966
  let candidate = baseName;
@@ -2017,15 +2087,12 @@ async function renameNest(nestId, patch) {
2017
2087
  const trimmed = plainName(patch.name);
2018
2088
  const slug = toSlug(trimmed);
2019
2089
  if (!slug) throw new ValidationError("Name must contain at least one alphanumeric character");
2020
- const conflict = await db.get(
2021
- "SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1",
2022
- [existing.user_id, slug, nestId]
2090
+ await assertSlugFree(
2091
+ existing.user_id,
2092
+ slug,
2093
+ "Another nest already uses this name. Pick a different one.",
2094
+ nestId
2023
2095
  );
2024
- if (conflict) {
2025
- throw new ValidationError(
2026
- `Another nest already uses this name. Pick a different one.`
2027
- );
2028
- }
2029
2096
  updates.push("name = ?", "slug = ?");
2030
2097
  params.push(trimmed, slug);
2031
2098
  }
@@ -2053,15 +2120,11 @@ async function createNest(userId, name, description) {
2053
2120
  "Name must contain at least one alphanumeric character"
2054
2121
  );
2055
2122
  }
2056
- const conflict = await db.get(
2057
- "SELECT id FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
2058
- [userId, slug]
2123
+ await assertSlugFree(
2124
+ userId,
2125
+ slug,
2126
+ "You already have a nest with this name. Pick a different one."
2059
2127
  );
2060
- if (conflict) {
2061
- throw new ValidationError(
2062
- "You already have a nest with this name. Pick a different one."
2063
- );
2064
- }
2065
2128
  const visibility = userId === ANON_USER_ID ? "public" : "private";
2066
2129
  await db.run(
2067
2130
  "INSERT INTO nests (id, user_id, name, slug, description, visibility) VALUES (?, ?, ?, ?, ?, ?)",
@@ -2079,6 +2142,11 @@ async function importNest(userId, name) {
2079
2142
  const db = getDb();
2080
2143
  const id = uuid3();
2081
2144
  const slug = toSlug(nm);
2145
+ await assertSlugFree(
2146
+ userId,
2147
+ slug,
2148
+ "A nest with this name already exists. Pick a different name."
2149
+ );
2082
2150
  const visibility = userId === ANON_USER_ID ? "public" : "private";
2083
2151
  mkdirSync(resolveNestPath(id), { recursive: true });
2084
2152
  await db.run(
@@ -2088,26 +2156,79 @@ async function importNest(userId, name) {
2088
2156
  trackEvent("nest.import", { nestId: id, userId });
2089
2157
  return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
2090
2158
  }
2091
- async function listNests(userId) {
2159
+ async function pinNest(userId, nestId) {
2160
+ const db = getDb();
2161
+ const existing = await db.get(
2162
+ "SELECT 1 FROM nest_pins WHERE user_id = ? AND nest_id = ?",
2163
+ [userId, nestId]
2164
+ );
2165
+ if (existing) return;
2166
+ try {
2167
+ await db.run(
2168
+ "INSERT INTO nest_pins (user_id, nest_id) VALUES (?, ?)",
2169
+ [userId, nestId]
2170
+ );
2171
+ } catch (err) {
2172
+ const now = await db.get(
2173
+ "SELECT 1 FROM nest_pins WHERE user_id = ? AND nest_id = ?",
2174
+ [userId, nestId]
2175
+ );
2176
+ if (!now) throw err;
2177
+ }
2178
+ }
2179
+ async function unpinNest(userId, nestId) {
2180
+ await getDb().run(
2181
+ "DELETE FROM nest_pins WHERE user_id = ? AND nest_id = ?",
2182
+ [userId, nestId]
2183
+ );
2184
+ }
2185
+ async function pinnedNestIds(userId) {
2186
+ const rows = await getDb().all(
2187
+ "SELECT nest_id FROM nest_pins WHERE user_id = ?",
2188
+ [userId]
2189
+ );
2190
+ return new Set(rows.map((r) => r.nest_id));
2191
+ }
2192
+ var LIVE_ONLY = "archived_at IS NULL";
2193
+ var LIVE_ONLY_N = "n.archived_at IS NULL";
2194
+ async function listNests(userId, opts = {}) {
2092
2195
  const db = getDb();
2196
+ const scope = opts.archived ? "archived_at IS NOT NULL" : LIVE_ONLY;
2093
2197
  if (userId === ANON_USER_ID) {
2094
2198
  return await db.all(
2095
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
2199
+ `SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY created_at DESC`,
2096
2200
  [ANON_USER_ID]
2097
2201
  );
2098
2202
  }
2099
2203
  const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
2100
2204
  if (!includeAnon) {
2101
2205
  return await db.all(
2102
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
2206
+ `SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY created_at DESC`,
2103
2207
  [userId]
2104
2208
  );
2105
2209
  }
2106
2210
  return await db.all(
2107
- "SELECT * FROM nests WHERE user_id = ? OR user_id = ? ORDER BY created_at DESC",
2211
+ `SELECT * FROM nests WHERE (user_id = ? OR user_id = ?) AND ${scope}
2212
+ ORDER BY created_at DESC`,
2108
2213
  [userId, ANON_USER_ID]
2109
2214
  );
2110
2215
  }
2216
+ async function archiveNest(nestId) {
2217
+ await getDb().transaction(async (tx) => {
2218
+ await tx.run(
2219
+ "UPDATE nests SET archived_at = ? WHERE id = ? AND archived_at IS NULL",
2220
+ [(/* @__PURE__ */ new Date()).toISOString(), nestId]
2221
+ );
2222
+ await tx.run(
2223
+ `DELETE FROM deletion_requests
2224
+ WHERE nest_id = ? AND target_type = 'nest_archive' AND status = 'pending'`,
2225
+ [nestId]
2226
+ );
2227
+ });
2228
+ }
2229
+ async function restoreNest(nestId) {
2230
+ await getDb().run("UPDATE nests SET archived_at = NULL WHERE id = ?", [nestId]);
2231
+ }
2111
2232
  async function nestsSharedViaTeams(userId) {
2112
2233
  const db = getDb();
2113
2234
  const teams = await db.all(
@@ -2129,6 +2250,7 @@ async function nestsSharedViaTeams(userId) {
2129
2250
  const candidates = await db.all(
2130
2251
  `SELECT * FROM nests
2131
2252
  WHERE user_id != ?
2253
+ AND ${LIVE_ONLY}
2132
2254
  AND (${ids.map(() => "shared_teams LIKE ?").join(" OR ")})`,
2133
2255
  [userId, ...ids.map((id) => `%${id}%`)]
2134
2256
  );
@@ -2154,6 +2276,7 @@ async function listSharedNests(userId) {
2154
2276
  LEFT JOIN grants g
2155
2277
  ON g.nest_id = n.id AND g.user_id = ?
2156
2278
  WHERE n.user_id != ?
2279
+ AND ${LIVE_ONLY_N}
2157
2280
  AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL OR g.id IS NOT NULL)
2158
2281
  ORDER BY n.created_at DESC`,
2159
2282
  [userId, userId, userId, userId]
@@ -2167,7 +2290,8 @@ async function listSharedNests(userId) {
2167
2290
  }
2168
2291
  if (await isServerAdminUserId(userId)) {
2169
2292
  const anon = await db.all(
2170
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
2293
+ `SELECT * FROM nests WHERE user_id = ? AND ${LIVE_ONLY}
2294
+ ORDER BY created_at DESC`,
2171
2295
  [ANON_USER_ID]
2172
2296
  );
2173
2297
  for (const n of anon) {
@@ -2201,11 +2325,12 @@ async function listVisibleNests(userId, opts = {}) {
2201
2325
  ON s.nest_id = n.id AND s.is_active = 1
2202
2326
  AND LOWER(s.user_email) = LOWER(u.email)
2203
2327
  ${opts.readable ? "LEFT JOIN grants g ON g.nest_id = n.id AND g.user_id = ?" : ""}
2204
- WHERE n.user_id = ?
2328
+ WHERE ${LIVE_ONLY_N}
2329
+ AND (n.user_id = ?
2205
2330
  OR nc.user_id IS NOT NULL
2206
2331
  OR s.id IS NOT NULL
2207
2332
  ${opts.readable ? "OR g.id IS NOT NULL OR n.visibility = 'public'" : ""}
2208
- ${includeAnon ? "OR n.user_id = ?" : ""}
2333
+ ${includeAnon ? "OR n.user_id = ?" : ""})
2209
2334
  ORDER BY n.created_at DESC`,
2210
2335
  params
2211
2336
  );
@@ -2223,6 +2348,7 @@ async function listPublicNests(userId) {
2223
2348
  return await db.all(
2224
2349
  `SELECT n.* FROM nests n
2225
2350
  WHERE n.visibility = 'public'
2351
+ AND ${LIVE_ONLY_N}
2226
2352
  AND n.user_id != ?
2227
2353
  AND NOT EXISTS (
2228
2354
  SELECT 1 FROM nest_collaborators nc
@@ -2250,6 +2376,10 @@ async function deleteNest(nestId) {
2250
2376
  } catch {
2251
2377
  }
2252
2378
  await tx.run("DELETE FROM api_keys WHERE nest_id = ?", [id]);
2379
+ try {
2380
+ await tx.run("DELETE FROM nest_pins WHERE nest_id = ?", [id]);
2381
+ } catch {
2382
+ }
2253
2383
  await tx.run("DELETE FROM nests WHERE id = ?", [id]);
2254
2384
  });
2255
2385
  const path = resolveNestPath(nestId);
@@ -2283,7 +2413,7 @@ var META_BASENAMES = /* @__PURE__ */ new Set([
2283
2413
  ]);
2284
2414
  function isMetaFile(idOrPath) {
2285
2415
  const base = idOrPath.split(/[/\\]/).pop() ?? "";
2286
- return META_BASENAMES.has(base.replace(/\.md$/i, ""));
2416
+ return META_BASENAMES.has(base.replace(/\.md$/i, "").toUpperCase());
2287
2417
  }
2288
2418
 
2289
2419
  // src/shared/batch.ts
@@ -2457,8 +2587,8 @@ async function ensureNodeIndex(nestId) {
2457
2587
  return run;
2458
2588
  }
2459
2589
  async function rebuildNodeIndex(nestId) {
2460
- const { engineCache: engineCache2 } = await import("./engine-VZTSDUPB.js");
2461
- const { documentsWithSuggestions } = await import("./external-edit-service-3FVK3OD7.js");
2590
+ const { engineCache: engineCache2 } = await import("./engine-WXWER2V7.js");
2591
+ const { documentsWithSuggestions } = await import("./external-edit-service-Y757EFMS.js");
2462
2592
  const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
2463
2593
  dropDiscoveryCache();
2464
2594
  const startedAt = writeGeneration.get(nestId) ?? 0;
@@ -2739,7 +2869,12 @@ export {
2739
2869
  renameNest,
2740
2870
  createNest,
2741
2871
  importNest,
2872
+ pinNest,
2873
+ unpinNest,
2874
+ pinnedNestIds,
2742
2875
  listNests,
2876
+ archiveNest,
2877
+ restoreNest,
2743
2878
  listSharedNests,
2744
2879
  listVisibleNests,
2745
2880
  listPublicNests,
@@ -2786,12 +2921,16 @@ export {
2786
2921
  replaceTeamRoster,
2787
2922
  updateMemberRole,
2788
2923
  removeMember,
2924
+ parseSharedTeams,
2925
+ listSharedTeamMembers,
2789
2926
  listNestTeams,
2790
2927
  shareTeamWithNest,
2791
2928
  unshareTeamFromNest,
2792
2929
  resolveTeamRolesForUser,
2793
2930
  isServerAdminUserId,
2794
2931
  resolveNestAccess,
2932
+ isNestArchived,
2933
+ capForArchive,
2795
2934
  resolveNestPermission,
2796
2935
  permissionLevel,
2797
2936
  isPublicReader
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-YVMSM7LS.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-XCFKS65Y.js";
6
+ } from "./chunk-SKZ7YYT4.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -11,7 +11,10 @@ var rank = (r) => r === "write" ? 2 : 1;
11
11
  async function resolveNodeGrant(nestId, userId, nodeId) {
12
12
  if (!userId || !nodeId) return null;
13
13
  const rows = await getDb().all(
14
- "SELECT target, role FROM grants WHERE nest_id = ? AND user_id = ?",
14
+ `SELECT g.target, g.role, n.archived_at
15
+ FROM grants g
16
+ JOIN nests n ON n.id = g.nest_id
17
+ WHERE g.nest_id = ? AND g.user_id = ?`,
15
18
  [nestId, userId]
16
19
  );
17
20
  let best = null;
@@ -20,6 +23,7 @@ async function resolveNodeGrant(nestId, userId, nodeId) {
20
23
  if (!best || rank(r.role) > rank(best)) best = r.role;
21
24
  }
22
25
  }
26
+ if (best === "write" && rows[0]?.archived_at) return "read";
23
27
  return best;
24
28
  }
25
29
  async function listUserGrants(nestId, userId) {
@@ -11,7 +11,7 @@ import {
11
11
  resolveNestPermission,
12
12
  resolveTeamRolesForUser,
13
13
  sendEmailToRecipient
14
- } from "./chunk-7CPP6FIU.js";
14
+ } from "./chunk-G4AL7ETF.js";
15
15
  import {
16
16
  ConflictError,
17
17
  ValidationError
@@ -20,7 +20,7 @@ import {
20
20
  config,
21
21
  getDb,
22
22
  isEmailish
23
- } from "./chunk-XCFKS65Y.js";
23
+ } from "./chunk-SKZ7YYT4.js";
24
24
 
25
25
  // src/governance/stewardship-service.ts
26
26
  import { v4 as uuid } from "uuid";