@promptowl/contextnest-community 1.7.0 → 1.8.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
@@ -13,10 +13,11 @@ import {
13
13
  getPendingReview,
14
14
  getReviewHistory,
15
15
  getReviewQueue,
16
+ notifySlackForNest,
16
17
  reject,
17
18
  safePublishDocument,
18
19
  submitForReview
19
- } from "./chunk-MOXICJPD.js";
20
+ } from "./chunk-Z3Y46ZGU.js";
20
21
  import {
21
22
  checkConflict,
22
23
  createVersion,
@@ -25,7 +26,7 @@ import {
25
26
  getDisplayStatus,
26
27
  getVersions,
27
28
  setApprovedVersion
28
- } from "./chunk-VD5QX2ZQ.js";
29
+ } from "./chunk-XNGOO6RI.js";
29
30
  import {
30
31
  AppError,
31
32
  ConflictError,
@@ -84,14 +85,14 @@ import {
84
85
  updateSteward,
85
86
  upsertEnvVar,
86
87
  validateLicense
87
- } from "./chunk-43DOX4LH.js";
88
+ } from "./chunk-PJTAUVD7.js";
88
89
  import {
89
90
  config,
90
91
  getDb,
91
92
  initDb,
92
93
  insertOrIgnore,
93
94
  nowExpr
94
- } from "./chunk-QMLAXQES.js";
95
+ } from "./chunk-PLAHCDQG.js";
95
96
  import {
96
97
  ANON_EMAIL,
97
98
  ANON_USER_ID
@@ -101,7 +102,8 @@ import {
101
102
  import { serve } from "@hono/node-server";
102
103
 
103
104
  // src/app.ts
104
- import { Hono as Hono9 } from "hono";
105
+ import { Hono as Hono10 } from "hono";
106
+ import { LinearRouter } from "hono/router/linear-router";
105
107
  import { createMiddleware as createMiddleware2 } from "hono/factory";
106
108
  import { cors } from "hono/cors";
107
109
 
@@ -1289,7 +1291,7 @@ async function approveExternalEdit(input) {
1289
1291
  const node = await storage.readDocument(input.documentId);
1290
1292
  const versionNum = result.versionEntry.version;
1291
1293
  const tags = node.frontmatter.tags || [];
1292
- const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-FJWVTZKR.js");
1294
+ const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-B3SDOJQE.js");
1293
1295
  await createVersion2({
1294
1296
  nestId: input.nestId,
1295
1297
  nodeId: input.documentId,
@@ -1375,6 +1377,10 @@ function bodyOnly(nodeId, raw) {
1375
1377
  return raw;
1376
1378
  }
1377
1379
  }
1380
+ var RUNNABLE_NODE_TYPES = ["agent", "skill"];
1381
+ function isRunnableType(type) {
1382
+ return RUNNABLE_NODE_TYPES.includes(type || "");
1383
+ }
1378
1384
  function toNodeResponse(node) {
1379
1385
  const fm = node.frontmatter;
1380
1386
  const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
@@ -1391,14 +1397,20 @@ function toNodeResponse(node) {
1391
1397
  created_at: fm.created_at,
1392
1398
  updated_at: fm.updated_at,
1393
1399
  content: node.body || "",
1394
- pendingChange: node.pendingChange ?? void 0
1400
+ pendingChange: node.pendingChange ?? void 0,
1401
+ // Same YAML-coercion rule as title above: `schedule: 123` parses as a
1402
+ // number — normalize to string at the API boundary.
1403
+ schedule: fm.metadata?.schedule != null ? String(fm.metadata.schedule) : void 0
1395
1404
  };
1396
1405
  }
1397
1406
  async function listNodesForCaller(nestId, userId, filters = {}) {
1398
1407
  const { storage, versions: versionManager } = await engineCache.get(nestId);
1399
1408
  let documents = await storage.discoverDocuments();
1400
1409
  if (filters.type) {
1401
- documents = documents.filter((n) => n.frontmatter.type === filters.type);
1410
+ const wanted = new Set(
1411
+ Array.isArray(filters.type) ? filters.type : [filters.type]
1412
+ );
1413
+ documents = documents.filter((n) => wanted.has(n.frontmatter.type));
1402
1414
  }
1403
1415
  if (filters.tag) {
1404
1416
  const tag = normalizeTag2(filters.tag);
@@ -1464,6 +1476,11 @@ async function createNode(nestId, input, userEmail) {
1464
1476
  }
1465
1477
  const now = (/* @__PURE__ */ new Date()).toISOString();
1466
1478
  const tags = (input.tags || []).map(normalizeTag2);
1479
+ if (input.schedule !== void 0 && !isRunnableType(input.type)) {
1480
+ throw new ValidationError(
1481
+ `schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(", ")})`
1482
+ );
1483
+ }
1467
1484
  const hasStewards = await isStewardshipEnabled(nestId);
1468
1485
  const initialStatus = hasStewards ? "draft" : "published";
1469
1486
  const initialVersion = hasStewards ? 1 : 0;
@@ -1478,7 +1495,11 @@ async function createNode(nestId, input, userEmail) {
1478
1495
  version: initialVersion,
1479
1496
  created_at: now,
1480
1497
  updated_at: now,
1481
- metadata: { owners: ["*"], scope: input.scope || "team" }
1498
+ metadata: {
1499
+ owners: ["*"],
1500
+ scope: input.scope || "team",
1501
+ ...input.schedule ? { schedule: input.schedule } : {}
1502
+ }
1482
1503
  },
1483
1504
  body: input.content,
1484
1505
  rawContent: ""
@@ -1605,6 +1626,17 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1605
1626
  if (patch.title) {
1606
1627
  node = { ...node, frontmatter: { ...node.frontmatter, title: patch.title } };
1607
1628
  }
1629
+ if (patch.schedule !== void 0) {
1630
+ if (!isRunnableType(node.frontmatter.type)) {
1631
+ throw new ValidationError(
1632
+ `schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(", ")})`
1633
+ );
1634
+ }
1635
+ const metadata = { ...node.frontmatter.metadata };
1636
+ if (patch.schedule === "") delete metadata.schedule;
1637
+ else metadata.schedule = patch.schedule;
1638
+ node = { ...node, frontmatter: { ...node.frontmatter, metadata } };
1639
+ }
1608
1640
  const hasStewards = await isStewardshipEnabled(nestId);
1609
1641
  const currentTags = node.frontmatter.tags || [];
1610
1642
  if (hasStewards && await getPendingReview(nestId, nodeId)) {
@@ -2165,6 +2197,10 @@ async function addCollaborator(params) {
2165
2197
  "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
2166
2198
  [collabId, nestId, userId, params.permission, granterId]
2167
2199
  );
2200
+ void notifySlackForNest(
2201
+ nestId,
2202
+ `:key: ${params.email || userId} added as *${params.permission}*${params.grantedByEmail ? ` by ${params.grantedByEmail}` : ""}`
2203
+ );
2168
2204
  return await db.get(
2169
2205
  "SELECT * FROM nest_collaborators WHERE id = ?",
2170
2206
  [collabId]
@@ -2597,6 +2633,7 @@ nodeRoutes.post("/", async (c) => {
2597
2633
  tags: body.tags,
2598
2634
  scope: body.scope,
2599
2635
  status: body.status,
2636
+ schedule: body.schedule,
2600
2637
  folder: body.folder
2601
2638
  },
2602
2639
  authorEmail
@@ -2614,7 +2651,7 @@ nodeRoutes.post("/", async (c) => {
2614
2651
  nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
2615
2652
  const nestId = c.req.param("nestId");
2616
2653
  const nodeId = c.req.param("nodeId");
2617
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-I2JAFYJU.js");
2654
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-ZXAMFTXO.js");
2618
2655
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
2619
2656
  nestId,
2620
2657
  nodeId
@@ -2635,7 +2672,7 @@ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
2635
2672
  nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
2636
2673
  const nestId = c.req.param("nestId");
2637
2674
  const nodeId = c.req.param("nodeId");
2638
- const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-FJWVTZKR.js");
2675
+ const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-B3SDOJQE.js");
2639
2676
  const allVersions = await getVersions2(nestId, nodeId);
2640
2677
  const approved = await getApprovedVersion2(nestId, nodeId);
2641
2678
  const db = getDb();
@@ -2667,7 +2704,7 @@ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
2667
2704
  nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
2668
2705
  const nestId = c.req.param("nestId");
2669
2706
  const nodeId = c.req.param("nodeId");
2670
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-2FSKW425.js");
2707
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-G5US3SMG.js");
2671
2708
  const history = await getReviewHistory2(nestId, nodeId);
2672
2709
  return c.json({ reviews: history });
2673
2710
  });
@@ -2814,7 +2851,8 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
2814
2851
  tags: body.tags,
2815
2852
  title: body.title,
2816
2853
  status: body.status,
2817
- changeNote: body.changeNote
2854
+ changeNote: body.changeNote,
2855
+ schedule: body.schedule
2818
2856
  },
2819
2857
  authorEmail
2820
2858
  );
@@ -2880,6 +2918,13 @@ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
2880
2918
  "DELETE FROM annotation_threads WHERE nest_id = ? AND node_id = ?",
2881
2919
  [nestId, derivedId]
2882
2920
  );
2921
+ await tx.run(
2922
+ `INSERT INTO node_deletions (nest_id, node_id, deleted_by, deleted_at)
2923
+ VALUES (?, ?, ?, ?)
2924
+ ON CONFLICT(nest_id, node_id) DO UPDATE
2925
+ SET deleted_by = excluded.deleted_by, deleted_at = excluded.deleted_at`,
2926
+ [nestId, nodeId, await getUserEmail(c), (/* @__PURE__ */ new Date()).toISOString()]
2927
+ );
2883
2928
  });
2884
2929
  await trackEvent("node.delete", { nestId, nodeId });
2885
2930
  return c.json({ deleted: true });
@@ -3388,6 +3433,100 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
3388
3433
 
3389
3434
  // src/nodes/query-routes.ts
3390
3435
  var queryRoutes = new Hono6();
3436
+ queryRoutes.get("/changes", async (c) => {
3437
+ const nestId = c.req.param("nestId");
3438
+ const userId = c.get("userId");
3439
+ const sinceRaw = c.req.query("since");
3440
+ const sinceMs = sinceRaw ? Date.parse(sinceRaw) : NaN;
3441
+ if (!sinceRaw || Number.isNaN(sinceMs)) {
3442
+ throw new ValidationError(
3443
+ "since must be an ISO-8601 date, e.g. ?since=2026-07-01T00:00:00Z"
3444
+ );
3445
+ }
3446
+ const includeContent = ["1", "true"].includes(
3447
+ c.req.query("include_content") || ""
3448
+ );
3449
+ const all = await listNodesForCaller(nestId, userId);
3450
+ const db = getDb();
3451
+ const publicReader = await isPublicReader(nestId, userId);
3452
+ const approvedRows = await db.all(
3453
+ "SELECT node_id, approved_at FROM approved_versions WHERE nest_id = ?",
3454
+ [nestId]
3455
+ );
3456
+ const parseDbTs = (t) => Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`);
3457
+ const approvedAtMsByNode = new Map(
3458
+ approvedRows.map((r) => [r.node_id, parseDbTs(r.approved_at)])
3459
+ );
3460
+ const publishedSince = new Set(
3461
+ approvedRows.filter((r) => parseDbTs(r.approved_at) > sinceMs).map((r) => r.node_id)
3462
+ );
3463
+ const summarize = (n, updatedOverride) => ({
3464
+ id: n.id,
3465
+ title: n.title,
3466
+ type: n.type,
3467
+ status: n.status,
3468
+ tags: n.tags,
3469
+ created_at: n.created_at,
3470
+ updated_at: updatedOverride ?? n.updated_at,
3471
+ ...includeContent ? { content: n.content } : {}
3472
+ });
3473
+ const created = [];
3474
+ const updated = [];
3475
+ for (const n of all) {
3476
+ if (publicReader) {
3477
+ const approvedMs = approvedAtMsByNode.get(n.id);
3478
+ if (approvedMs === void 0) continue;
3479
+ const approvedIso = new Date(approvedMs).toISOString();
3480
+ const createdAt2 = Date.parse(n.created_at || "");
3481
+ if (!Number.isNaN(createdAt2) && createdAt2 > sinceMs) {
3482
+ created.push(summarize(n, approvedIso));
3483
+ } else if (approvedMs > sinceMs) {
3484
+ updated.push(summarize(n, approvedIso));
3485
+ }
3486
+ continue;
3487
+ }
3488
+ const createdAt = Date.parse(n.created_at || "");
3489
+ const updatedAt = Date.parse(n.updated_at || "");
3490
+ if (!Number.isNaN(createdAt) && createdAt > sinceMs) created.push(summarize(n));
3491
+ else if (!Number.isNaN(updatedAt) && updatedAt > sinceMs) updated.push(summarize(n));
3492
+ else if (publishedSince.has(n.id)) updated.push(summarize(n));
3493
+ }
3494
+ const canSeeDeleted = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
3495
+ let deleted = null;
3496
+ if (canSeeDeleted) {
3497
+ const rows = await db.all(
3498
+ "SELECT node_id, deleted_by, deleted_at FROM node_deletions WHERE nest_id = ? AND deleted_at > ?",
3499
+ [nestId, new Date(sinceMs).toISOString()]
3500
+ );
3501
+ const liveIds = new Set(all.map((n) => n.id));
3502
+ deleted = rows.filter((r) => !liveIds.has(r.node_id)).map((r) => ({ id: r.node_id, deleted_by: r.deleted_by, deleted_at: r.deleted_at }));
3503
+ }
3504
+ return c.json({
3505
+ since: new Date(sinceMs).toISOString(),
3506
+ now: (/* @__PURE__ */ new Date()).toISOString(),
3507
+ created,
3508
+ updated,
3509
+ deleted
3510
+ });
3511
+ });
3512
+ queryRoutes.get("/runnables", async (c) => {
3513
+ const nestId = c.req.param("nestId");
3514
+ const userId = c.get("userId");
3515
+ const all = await listNodesForCaller(nestId, userId, {
3516
+ type: RUNNABLE_NODE_TYPES
3517
+ });
3518
+ const runnables = all.map((n) => ({
3519
+ id: n.id,
3520
+ title: n.title,
3521
+ type: n.type,
3522
+ schedule: n.schedule ?? null,
3523
+ status: n.status,
3524
+ tags: n.tags,
3525
+ updated_at: n.updated_at,
3526
+ content: n.content
3527
+ }));
3528
+ return c.json({ count: runnables.length, runnables });
3529
+ });
3391
3530
  queryRoutes.get("/graph", async (c) => {
3392
3531
  const nestId = c.req.param("nestId");
3393
3532
  const userId = c.get("userId");
@@ -3779,6 +3918,79 @@ import { Hono as Hono7 } from "hono";
3779
3918
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3780
3919
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3781
3920
 
3921
+ // src/telemetry/trace-log.ts
3922
+ var RETENTION_DAYS = 14;
3923
+ var PRUNE_EVERY = 500;
3924
+ var insertsSincePrune = 0;
3925
+ async function logTraceEvent(e) {
3926
+ try {
3927
+ const db = getDb();
3928
+ await db.run(
3929
+ `INSERT INTO api_events
3930
+ (ts, kind, method, path, tool, nest_id, user_id, user_email, status, duration_ms)
3931
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3932
+ [
3933
+ (/* @__PURE__ */ new Date()).toISOString(),
3934
+ e.kind,
3935
+ e.method ?? null,
3936
+ e.path ?? null,
3937
+ e.tool ?? null,
3938
+ e.nestId ?? null,
3939
+ e.userId ?? null,
3940
+ e.userEmail ?? null,
3941
+ e.status ?? null,
3942
+ e.durationMs ?? null
3943
+ ]
3944
+ );
3945
+ if (++insertsSincePrune >= PRUNE_EVERY) {
3946
+ insertsSincePrune = 0;
3947
+ const cutoff = new Date(
3948
+ Date.now() - RETENTION_DAYS * 864e5
3949
+ ).toISOString();
3950
+ await db.run("DELETE FROM api_events WHERE ts < ?", [cutoff]);
3951
+ }
3952
+ } catch {
3953
+ }
3954
+ }
3955
+ async function listTraceEvents(filters = {}) {
3956
+ const db = getDb();
3957
+ const limit = Number.isFinite(filters.limit) ? Math.min(Math.max(filters.limit, 1), 1e3) : 25;
3958
+ const offset = Number.isFinite(filters.offset) ? Math.max(filters.offset, 0) : 0;
3959
+ const from = "FROM api_events e LEFT JOIN users u ON u.id = e.user_id";
3960
+ const where = [];
3961
+ const args = [];
3962
+ if (filters.kind) {
3963
+ where.push("e.kind = ?");
3964
+ args.push(filters.kind);
3965
+ }
3966
+ if (filters.nestId) {
3967
+ where.push("e.nest_id = ?");
3968
+ args.push(filters.nestId);
3969
+ }
3970
+ if (filters.user) {
3971
+ where.push("(COALESCE(e.user_email, u.email) LIKE ? OR e.user_id = ?)");
3972
+ args.push(`%${filters.user}%`, filters.user);
3973
+ }
3974
+ const whereSql = where.length ? ` WHERE ${where.join(" AND ")}` : "";
3975
+ try {
3976
+ const totalRow = await db.get(
3977
+ `SELECT COUNT(*) AS n ${from}${whereSql}`,
3978
+ args
3979
+ );
3980
+ const total = Number(totalRow?.n ?? 0);
3981
+ const events = await db.all(
3982
+ `SELECT e.*, COALESCE(e.user_email, u.email) AS caller
3983
+ ${from}${whereSql}
3984
+ ORDER BY e.id DESC
3985
+ LIMIT ? OFFSET ?`,
3986
+ [...args, limit, offset]
3987
+ );
3988
+ return { events, total };
3989
+ } catch {
3990
+ return { events: [], total: 0 };
3991
+ }
3992
+ }
3993
+
3782
3994
  // src/mcp/tools.ts
3783
3995
  var MAX_HOPS = 10;
3784
3996
  function normalizeHops(raw) {
@@ -4084,6 +4296,26 @@ async function resolveLlmBody(ctx, node) {
4084
4296
  }
4085
4297
  }
4086
4298
  async function handleToolCall(toolName, args, ctx) {
4299
+ const started = Date.now();
4300
+ let status = 200;
4301
+ try {
4302
+ return await runTool(toolName, args, ctx);
4303
+ } catch (err) {
4304
+ status = 500;
4305
+ throw err;
4306
+ } finally {
4307
+ void logTraceEvent({
4308
+ kind: "mcp",
4309
+ tool: toolName,
4310
+ nestId: ctx.nestId,
4311
+ userId: ctx.userId,
4312
+ userEmail: ctx.userEmail,
4313
+ status,
4314
+ durationMs: Date.now() - started
4315
+ });
4316
+ }
4317
+ }
4318
+ async function runTool(toolName, args, ctx) {
4087
4319
  const { storage, queryEngine, versionManager, nestId, userId, userEmail } = ctx;
4088
4320
  switch (toolName) {
4089
4321
  case "context_init": {
@@ -4627,8 +4859,407 @@ mcpRoutes.all("/", async (c) => {
4627
4859
  }
4628
4860
  });
4629
4861
 
4630
- // src/governance/routes.ts
4862
+ // src/mcp/server-routes.ts
4631
4863
  import { Hono as Hono8 } from "hono";
4864
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
4865
+ import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4866
+ import { z as z2 } from "zod";
4867
+ async function getUserEmail3(userId) {
4868
+ const db = getDb();
4869
+ const user = await db.get("SELECT email FROM users WHERE id = ?", [
4870
+ userId
4871
+ ]);
4872
+ return user?.email || "anonymous@localhost";
4873
+ }
4874
+ async function canRead(nestId, userId) {
4875
+ if (config.AUTH_MODE === "open") return true;
4876
+ const perm = await resolveNestPermission(nestId, userId);
4877
+ return permissionLevel(perm) >= permissionLevel("read");
4878
+ }
4879
+ async function resolveNestRef(ref, userId) {
4880
+ const db = getDb();
4881
+ const byId = await db.get("SELECT * FROM nests WHERE id = ?", [ref]);
4882
+ if (byId) return byId;
4883
+ const byName = await db.all(
4884
+ "SELECT * FROM nests WHERE LOWER(name) = LOWER(?) OR LOWER(slug) = LOWER(?)",
4885
+ [ref, ref]
4886
+ );
4887
+ if (byName.length <= 1) return byName[0] ?? null;
4888
+ const readable = (await Promise.all(
4889
+ byName.map(async (n) => await canRead(n.id, userId) ? n : null)
4890
+ )).filter((n) => n !== null);
4891
+ return readable.length === 1 ? readable[0] : null;
4892
+ }
4893
+ async function buildNestIndex(userId, nestScope) {
4894
+ const [owned, shared, publicExtras] = await Promise.all([
4895
+ listNests(userId),
4896
+ listSharedNests(userId),
4897
+ listPublicNests(userId)
4898
+ ]);
4899
+ const seen = /* @__PURE__ */ new Set();
4900
+ const rows = [];
4901
+ for (const n of [...owned, ...shared, ...publicExtras]) {
4902
+ if (seen.has(n.id)) continue;
4903
+ if (nestScope && n.id !== nestScope) continue;
4904
+ seen.add(n.id);
4905
+ rows.push(n);
4906
+ }
4907
+ return Promise.all(
4908
+ rows.map(async (n) => {
4909
+ let permission = "read";
4910
+ try {
4911
+ permission = config.AUTH_MODE === "open" ? "owner" : await resolveNestPermission(n.id, userId);
4912
+ } catch {
4913
+ }
4914
+ let document_count = null;
4915
+ try {
4916
+ const { storage } = await engineCache.get(n.id);
4917
+ document_count = (await storage.discoverDocuments()).length;
4918
+ } catch {
4919
+ }
4920
+ return {
4921
+ id: n.id,
4922
+ name: n.name,
4923
+ description: n.description,
4924
+ visibility: n.visibility,
4925
+ permission,
4926
+ document_count
4927
+ };
4928
+ })
4929
+ );
4930
+ }
4931
+ function renderNestIndexMarkdown(nests, baseUrl) {
4932
+ const lines = [
4933
+ "# Nest index",
4934
+ "",
4935
+ `${nests.length} nest${nests.length === 1 ? "" : "s"} accessible with these credentials. Full interaction manual: ${baseUrl}/llms.txt`,
4936
+ ""
4937
+ ];
4938
+ for (const n of nests) {
4939
+ lines.push(`## ${n.name}`);
4940
+ lines.push("");
4941
+ lines.push(`- id: \`${n.id}\``);
4942
+ if (n.description) lines.push(`- description: ${n.description}`);
4943
+ lines.push(`- permission: ${n.permission} \xB7 visibility: ${n.visibility}`);
4944
+ if (n.document_count !== null)
4945
+ lines.push(`- documents: ${n.document_count}`);
4946
+ lines.push(
4947
+ `- retrieve: \`POST ${baseUrl}/nests/${n.id}/context\` \xB7 mcp: \`${baseUrl}/nests/${n.id}/mcp\` \xB7 export: \`${baseUrl}/nests/${n.id}/export?format=markdown\``
4948
+ );
4949
+ lines.push("");
4950
+ }
4951
+ return lines.join("\n");
4952
+ }
4953
+ var NEST_ARG = {
4954
+ nest: {
4955
+ type: "string",
4956
+ description: "Target nest \u2014 id, or exact name/slug (case-insensitive)"
4957
+ }
4958
+ };
4959
+ var SERVER_TOOL_DEFINITIONS = [
4960
+ {
4961
+ name: "nest_index",
4962
+ description: "Grounded index of every nest these credentials can read: name, id, permission, document count. Call this FIRST, then target a nest with the other tools.",
4963
+ inputSchema: { type: "object", properties: {} }
4964
+ },
4965
+ {
4966
+ name: "context_query",
4967
+ description: "Run a structured selector query against one nest. Selector grammar: #tag, type:X, [[Title]], scope:X, combined with +AND, |OR, -NOT.",
4968
+ inputSchema: {
4969
+ type: "object",
4970
+ properties: {
4971
+ ...NEST_ARG,
4972
+ query: { type: "string", description: "Selector query" },
4973
+ hops: {
4974
+ type: "number",
4975
+ description: "Graph traversal depth from the matches (default: 2)"
4976
+ }
4977
+ },
4978
+ required: ["nest", "query"]
4979
+ }
4980
+ },
4981
+ {
4982
+ name: "context_search",
4983
+ description: "Full-text keyword search across one nest.",
4984
+ inputSchema: {
4985
+ type: "object",
4986
+ properties: {
4987
+ ...NEST_ARG,
4988
+ query: { type: "string", description: "Search terms" }
4989
+ },
4990
+ required: ["nest", "query"]
4991
+ }
4992
+ },
4993
+ {
4994
+ name: "context_get",
4995
+ description: "Get the FULL content of a node in a nest by title or id.",
4996
+ inputSchema: {
4997
+ type: "object",
4998
+ properties: {
4999
+ ...NEST_ARG,
5000
+ title: { type: "string", description: "Title of the node" },
5001
+ id: { type: "string", description: "ID of the node" }
5002
+ },
5003
+ required: ["nest"]
5004
+ }
5005
+ },
5006
+ {
5007
+ name: "context_list",
5008
+ description: "Browse one nest's contents with optional type, tag, or limit filters.",
5009
+ inputSchema: {
5010
+ type: "object",
5011
+ properties: {
5012
+ ...NEST_ARG,
5013
+ type: { type: "string", description: "Filter by node type" },
5014
+ tag: { type: "string", description: "Filter by tag" },
5015
+ limit: { type: "number", description: "Max nodes to return" }
5016
+ },
5017
+ required: ["nest"]
5018
+ }
5019
+ },
5020
+ {
5021
+ name: "context_overview",
5022
+ description: "Map of one nest: node count, types, tags, title+snippet per node.",
5023
+ inputSchema: {
5024
+ type: "object",
5025
+ properties: { ...NEST_ARG },
5026
+ required: ["nest"]
5027
+ }
5028
+ },
5029
+ {
5030
+ name: "context_resolve",
5031
+ description: "Full deterministic context resolution against one nest \u2014 selector + hops + token budget, complete node bodies.",
5032
+ inputSchema: {
5033
+ type: "object",
5034
+ properties: {
5035
+ ...NEST_ARG,
5036
+ selector: { type: "string", description: "Selector query string" },
5037
+ max_tokens: {
5038
+ type: "number",
5039
+ description: "Approximate token budget (default: 8000)"
5040
+ },
5041
+ hops: {
5042
+ type: "number",
5043
+ description: "Graph traversal depth from the matches (default: 2)"
5044
+ }
5045
+ },
5046
+ required: ["nest", "selector"]
5047
+ }
5048
+ }
5049
+ ];
5050
+ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope, baseUrl) {
5051
+ if (toolName === "nest_index") {
5052
+ const nests = await buildNestIndex(userId, nestScope);
5053
+ if (!nests.length) return "No nests accessible with these credentials.";
5054
+ return renderNestIndexMarkdown(nests, baseUrl);
5055
+ }
5056
+ const inaccessible = `Nest not found or not accessible: "${args.nest}". Use nest_index to see what these credentials can read.`;
5057
+ const nest = await resolveNestRef(String(args.nest ?? ""), userId);
5058
+ if (!nest) return inaccessible;
5059
+ if (nestScope && nest.id !== nestScope) return inaccessible;
5060
+ if (!await canRead(nest.id, userId)) return inaccessible;
5061
+ const engine = await engineCache.get(nest.id);
5062
+ return handleToolCall(toolName, args, {
5063
+ storage: engine.storage,
5064
+ queryEngine: engine.query,
5065
+ versionManager: engine.versions,
5066
+ nestId: nest.id,
5067
+ userId,
5068
+ userEmail
5069
+ });
5070
+ }
5071
+ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
5072
+ const server = new McpServer2(
5073
+ { name: "contextnest-server", version: "1.0.0" },
5074
+ { capabilities: { tools: {} } }
5075
+ );
5076
+ for (const tool of SERVER_TOOL_DEFINITIONS) {
5077
+ const props = tool.inputSchema.properties || {};
5078
+ const required = tool.inputSchema.required || [];
5079
+ const shape = {};
5080
+ for (const [key, def] of Object.entries(props)) {
5081
+ let field;
5082
+ if (def.type === "string") field = z2.string();
5083
+ else if (def.type === "number") field = z2.number();
5084
+ else if (def.type === "array") field = z2.array(z2.string());
5085
+ else field = z2.any();
5086
+ if (!required.includes(key)) field = field.optional();
5087
+ shape[key] = field;
5088
+ }
5089
+ server.tool(tool.name, tool.description, shape, async (args) => {
5090
+ const text = await handleServerToolCall(
5091
+ tool.name,
5092
+ args,
5093
+ userId,
5094
+ userEmail,
5095
+ nestScope,
5096
+ baseUrl
5097
+ );
5098
+ return { content: [{ type: "text", text }] };
5099
+ });
5100
+ }
5101
+ return server;
5102
+ }
5103
+ function requestBaseUrl(reqUrl) {
5104
+ if (config.PUBLIC_BASE_URL) return config.PUBLIC_BASE_URL;
5105
+ try {
5106
+ return new URL(reqUrl).origin;
5107
+ } catch {
5108
+ return "";
5109
+ }
5110
+ }
5111
+ var serverMcpRoutes = new Hono8();
5112
+ serverMcpRoutes.all("/", async (c) => {
5113
+ const userId = c.get("userId");
5114
+ const userEmail = await getUserEmail3(userId);
5115
+ const server = createServerMcp(
5116
+ userId,
5117
+ userEmail,
5118
+ c.get("nestScope"),
5119
+ requestBaseUrl(c.req.url)
5120
+ );
5121
+ const transport = new WebStandardStreamableHTTPServerTransport2({
5122
+ sessionIdGenerator: void 0,
5123
+ enableJsonResponse: true
5124
+ });
5125
+ await server.server.connect(transport);
5126
+ try {
5127
+ return await transport.handleRequest(c.req.raw);
5128
+ } finally {
5129
+ await transport.close();
5130
+ await server.server.close();
5131
+ }
5132
+ });
5133
+ var nestIndexRoutes = new Hono8();
5134
+ nestIndexRoutes.get("/", async (c) => {
5135
+ const userId = c.get("userId");
5136
+ const nests = await buildNestIndex(userId, c.get("nestScope"));
5137
+ if (c.req.query("format") === "json") {
5138
+ return c.json({ count: nests.length, nests });
5139
+ }
5140
+ const md = renderNestIndexMarkdown(nests, requestBaseUrl(c.req.url));
5141
+ return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
5142
+ });
5143
+ function renderLlmsTxt(baseUrl) {
5144
+ const B = baseUrl || "<server-url>";
5145
+ return `# ContextNest Community Server
5146
+
5147
+ Self-hosted context governance server. Nests are versioned knowledge vaults
5148
+ of markdown documents with tags, wiki-links ([[Title]]), and steward
5149
+ governance. This file is the complete interaction manual for agents.
5150
+
5151
+ ## Authentication
5152
+
5153
+ Every request (except this file and /health): \`Authorization: Bearer cnst_<api-key>\`.
5154
+ Keys are minted in the web UI (Connect dialog \u2192 Generate Key). A key is
5155
+ either user-level (sees every nest the user can read) or scoped to one nest.
5156
+ Servers in "open" auth mode need no key.
5157
+
5158
+ ## Start here
5159
+
5160
+ 1. \`GET ${B}/index\` \u2014 your grounded nest index (markdown; \`?format=json\` for JSON).
5161
+ One entry per accessible nest: name, id, permission, document count, and
5162
+ the per-nest URLs below.
5163
+ 2. Pick a nest id, then retrieve with the API or MCP.
5164
+
5165
+ ## Deterministic retrieval API
5166
+
5167
+ Same inputs \u2192 same context, every time. The primary call:
5168
+
5169
+ \`\`\`
5170
+ POST ${B}/nests/<nest-id>/context
5171
+ Content-Type: application/json
5172
+ { "selector": "#gtm", "hops": 2, "max_tokens": 4000, "include_drafts": false }
5173
+ \`\`\`
5174
+
5175
+ Returns \`{ context, nodes[], trace }\` \u2014 assembled markdown plus a trace with
5176
+ \`hops_used\` and \`nodes_traversed\` so you can audit exactly what was pulled.
5177
+
5178
+ Selector grammar:
5179
+ - \`#tag\` \u2014 nodes carrying a tag
5180
+ - \`type:document\` \u2014 nodes of a type
5181
+ - \`[[Title]]\` \u2014 a node by exact title
5182
+ - \`scope:team\` \u2014 nodes by visibility scope
5183
+ - Combine: \`+AND\`, \`|OR\`, \`-NOT\` (e.g. \`#gtm+type:document-#draft\`)
5184
+
5185
+ Graph-hop logic: \`hops\` is the traversal depth over wiki-links from the
5186
+ selector matches. \`hops: 0\` = exactly the matches; \`hops: 1\` = matches plus
5187
+ directly linked nodes; \`hops: 2\` (default) and up walk the link graph
5188
+ further out. For an exact set with no expansion, OR the selectors together
5189
+ (\`[[A]]|[[B]]\`) and pass \`hops: 0\`.
5190
+
5191
+ Skip logic \u2014 what retrieval excludes and why:
5192
+ - Draft/unapproved documents are skipped unless \`include_drafts: true\`
5193
+ (and public/read-only callers always get approved content only).
5194
+ - \`max_tokens\` is a budget: documents that don't fit are skipped, and the
5195
+ trace reports \`truncated_by_budget\` so silence never means "complete".
5196
+ - Documents are deduped by id when a title match and a selector match overlap.
5197
+
5198
+ Other read endpoints (all under \`${B}/nests/<nest-id>\`):
5199
+ - \`POST /query\` \u2014 \`{ "query": "<selector>", "hops": N }\`; matches + snippets, no bodies
5200
+ - \`GET /search?q=<terms>\` \u2014 full-text search
5201
+ - \`GET /overview\` \u2014 node count, types, tags
5202
+ - \`GET /nodes\` \u2014 list; \`GET /nodes/<node-id>?format=markdown\` \u2014 one document
5203
+ - \`GET /export?format=markdown\` \u2014 whole nest as one markdown file (optional \`selector\`, \`max_tokens\`)
5204
+ - \`GET /graph\` \u2014 ontology graph (nodes, links, tags, stewards)
5205
+
5206
+ Write endpoints (need write permission):
5207
+ - \`POST /nodes\` \u2014 \`{ "title", "content", "tags"?, "type"?, "folder"? }\`
5208
+ - \`PATCH /nodes/<node-id>\` \u2014 \`{ "content"? , "append"?, "tags"?, "title"? }\`
5209
+ - \`DELETE /nodes/<node-id>\`
5210
+ - Governance: \`POST /nodes/<id>/submit-review\`, \`/approve\`, \`/reject\`
5211
+
5212
+ ## MCP (Model Context Protocol)
5213
+
5214
+ Streamable HTTP, stateless. Two endpoints:
5215
+
5216
+ - \`${B}/mcp\` \u2014 server-level, one connection for everything readable.
5217
+ Tools: \`nest_index\` (call first), then \`context_query\`, \`context_search\`,
5218
+ \`context_get\`, \`context_list\`, \`context_overview\`, \`context_resolve\` \u2014
5219
+ each takes a \`nest\` argument (id or exact name) plus the same
5220
+ selector/hops/max_tokens parameters as the REST API. Read-only.
5221
+ - \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, full toolset including writes and
5222
+ governance (create/update, submit/approve/reject, stewards, sharing).
5223
+
5224
+ Claude Code / Cursor / VS Code connect natively via HTTP with the
5225
+ \`Authorization: Bearer\` header. Claude Desktop needs the mcp-remote bridge:
5226
+
5227
+ \`\`\`
5228
+ npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
5229
+ \`\`\`
5230
+
5231
+ ## ctx CLI (npm: @promptowl/contextnest-cli)
5232
+
5233
+ \`npm i -g @promptowl/contextnest-cli\`
5234
+
5235
+ - \`ctx push --server ${B} --nest <nest-id> --key <api-key>\` \u2014 upload a local
5236
+ vault into a nest (add \`--include-drafts\` for unpublished nodes).
5237
+ - All other ctx commands (\`query\`, \`add\`, \`search\`, \`checkpoint\`) operate on
5238
+ a LOCAL vault directory only. For remote reads use the API or MCP above \u2014
5239
+ there is currently no remote \`ctx query\`.
5240
+
5241
+ ## npm packages
5242
+
5243
+ - \`@promptowl/contextnest-community\` \u2014 this server
5244
+ - \`@promptowl/contextnest-cli\` \u2014 the ctx CLI
5245
+ - \`@promptowl/contextnest-engine\` \u2014 vault storage + selector/graph engine
5246
+
5247
+ ## Conventions
5248
+
5249
+ - Node ids are paths: \`nodes/<slug>\` or nested \`nodes/<folder>/<slug>\`.
5250
+ - Tags are \`#lowercase\`. Wiki-links are \`[[Exact Title]]\`.
5251
+ - Every write is versioned and hash-chained; governed nests route writes
5252
+ through steward review before they become AI-visible.
5253
+ `;
5254
+ }
5255
+ var llmsTxtRoutes = new Hono8();
5256
+ llmsTxtRoutes.get("/", (c) => {
5257
+ const md = renderLlmsTxt(requestBaseUrl(c.req.url));
5258
+ return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
5259
+ });
5260
+
5261
+ // src/governance/routes.ts
5262
+ import { Hono as Hono9 } from "hono";
4632
5263
 
4633
5264
  // src/governance/comment-service.ts
4634
5265
  import { v4 as uuid4 } from "uuid";
@@ -4932,7 +5563,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
4932
5563
  if (Number.isNaN(n) || n < 1) return def;
4933
5564
  return Math.min(n, max);
4934
5565
  }
4935
- var governanceRoutes = new Hono8();
5566
+ var governanceRoutes = new Hono9();
4936
5567
  governanceRoutes.get("/stewards", async (c) => {
4937
5568
  const nestId = c.req.param("nestId");
4938
5569
  const scope = c.req.query("scope");
@@ -4961,7 +5592,7 @@ governanceRoutes.get("/stewards", async (c) => {
4961
5592
  governanceRoutes.post("/stewards", async (c) => {
4962
5593
  const nestId = c.req.param("nestId");
4963
5594
  const body = await c.req.json();
4964
- const assignedBy = await getUserEmail3(c);
5595
+ const assignedBy = await getUserEmail4(c);
4965
5596
  if (!body.scope) throw new ValidationError("scope is required");
4966
5597
  if (body.scope === "folder") {
4967
5598
  throw new ValidationError(
@@ -5036,7 +5667,7 @@ governanceRoutes.get("/review-queue", async (c) => {
5036
5667
  limit,
5037
5668
  offset
5038
5669
  });
5039
- const email = await getUserEmail3(c);
5670
+ const email = await getUserEmail4(c);
5040
5671
  const canReviewCache = /* @__PURE__ */ new Map();
5041
5672
  const requests = [];
5042
5673
  for (const r of result.requests) {
@@ -5060,7 +5691,7 @@ governanceRoutes.get("/external-edits", async (c) => {
5060
5691
  });
5061
5692
  governanceRoutes.post("/external-edits/scan", async (c) => {
5062
5693
  const nestId = c.req.param("nestId");
5063
- const actor = await getUserEmail3(c);
5694
+ const actor = await getUserEmail4(c);
5064
5695
  const result = await scanNestForDrift(nestId, actor);
5065
5696
  return c.json(result);
5066
5697
  });
@@ -5070,7 +5701,7 @@ governanceRoutes.get("/activity", async (c) => {
5070
5701
  const activity = await getActivity({ nestId, limit });
5071
5702
  return c.json({ activity });
5072
5703
  });
5073
- var governanceNodeRoutes = new Hono8();
5704
+ var governanceNodeRoutes = new Hono9();
5074
5705
  governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
5075
5706
  const nestId = c.req.param("nestId");
5076
5707
  const nodeId = c.req.param("nodeId");
@@ -5129,7 +5760,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
5129
5760
  const nestId = c.req.param("nestId");
5130
5761
  const nodeId = c.req.param("nodeId");
5131
5762
  const body = await c.req.json();
5132
- const author = await getUserEmail3(c);
5763
+ const author = await getUserEmail4(c);
5133
5764
  try {
5134
5765
  const comment = await createComment({
5135
5766
  nestId,
@@ -5153,7 +5784,7 @@ governanceNodeRoutes.post(
5153
5784
  const nestId = c.req.param("nestId");
5154
5785
  const nodeId = c.req.param("nodeId");
5155
5786
  const commentId = c.req.param("commentId");
5156
- const resolvedBy = await getUserEmail3(c);
5787
+ const resolvedBy = await getUserEmail4(c);
5157
5788
  try {
5158
5789
  const comment = await resolveComment({
5159
5790
  nestId,
@@ -5186,7 +5817,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
5186
5817
  if (currentVersion === 0) {
5187
5818
  throw new ValidationError("Node has no versions to review");
5188
5819
  }
5189
- const userEmail = await getUserEmail3(c);
5820
+ const userEmail = await getUserEmail4(c);
5190
5821
  let request;
5191
5822
  try {
5192
5823
  request = await submitForReview({
@@ -5221,7 +5852,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
5221
5852
  const nestId = c.req.param("nestId");
5222
5853
  const nodeId = c.req.param("nodeId");
5223
5854
  const body = await c.req.json();
5224
- const userEmail = await getUserEmail3(c);
5855
+ const userEmail = await getUserEmail4(c);
5225
5856
  const isAdmin = isSuperAdmin(userEmail);
5226
5857
  try {
5227
5858
  const request = await approve({
@@ -5244,7 +5875,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
5244
5875
  if (!body.note) {
5245
5876
  throw new ValidationError("Rejection note is required");
5246
5877
  }
5247
- const userEmail = await getUserEmail3(c);
5878
+ const userEmail = await getUserEmail4(c);
5248
5879
  try {
5249
5880
  const request = await reject({
5250
5881
  nestId,
@@ -5261,19 +5892,19 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
5261
5892
  governanceNodeRoutes.get("/:nodeId{.+}/can-access", async (c) => {
5262
5893
  const nestId = c.req.param("nestId");
5263
5894
  const nodeId = c.req.param("nodeId");
5264
- const userEmail = await getUserEmail3(c);
5895
+ const userEmail = await getUserEmail4(c);
5265
5896
  return c.json(await canUserAccess(nestId, nodeId, userEmail));
5266
5897
  });
5267
5898
  governanceNodeRoutes.get("/:nodeId{.+}/can-approve", async (c) => {
5268
5899
  const nestId = c.req.param("nestId");
5269
5900
  const nodeId = c.req.param("nodeId");
5270
- const userEmail = await getUserEmail3(c);
5901
+ const userEmail = await getUserEmail4(c);
5271
5902
  return c.json(await canUserApprove(nestId, nodeId, userEmail));
5272
5903
  });
5273
5904
  governanceNodeRoutes.get("/:nodeId{.+}/can-edit", async (c) => {
5274
5905
  const nestId = c.req.param("nestId");
5275
5906
  const nodeId = c.req.param("nodeId");
5276
- const userEmail = await getUserEmail3(c);
5907
+ const userEmail = await getUserEmail4(c);
5277
5908
  return c.json(await canUserEdit(nestId, nodeId, userEmail));
5278
5909
  });
5279
5910
  governanceNodeRoutes.get("/:nodeId{.+?}/external-edits", async (c) => {
@@ -5306,7 +5937,7 @@ governanceNodeRoutes.post(
5306
5937
  const nodeId = c.req.param("nodeId");
5307
5938
  const suggestionId = c.req.param("suggestionId");
5308
5939
  const body = await c.req.json().catch(() => ({}));
5309
- const actor = await getUserEmail3(c);
5940
+ const actor = await getUserEmail4(c);
5310
5941
  try {
5311
5942
  const result = await approveExternalEdit({
5312
5943
  nestId,
@@ -5342,7 +5973,7 @@ governanceNodeRoutes.post(
5342
5973
  if (!body.reason) {
5343
5974
  throw new ValidationError("Rejection reason is required");
5344
5975
  }
5345
- const actor = await getUserEmail3(c);
5976
+ const actor = await getUserEmail4(c);
5346
5977
  try {
5347
5978
  const result = await rejectExternalEdit({
5348
5979
  nestId,
@@ -5363,7 +5994,7 @@ governanceNodeRoutes.post(
5363
5994
  governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
5364
5995
  const nestId = c.req.param("nestId");
5365
5996
  const nodeId = c.req.param("nodeId");
5366
- const userEmail = await getUserEmail3(c);
5997
+ const userEmail = await getUserEmail4(c);
5367
5998
  const request = await cancelReview({
5368
5999
  nestId,
5369
6000
  nodeId,
@@ -5371,7 +6002,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
5371
6002
  });
5372
6003
  return c.json({ review: request });
5373
6004
  });
5374
- async function getUserEmail3(c) {
6005
+ async function getUserEmail4(c) {
5375
6006
  const userId = c.get("userId");
5376
6007
  const db = getDb();
5377
6008
  const user = await db.get(
@@ -5381,6 +6012,30 @@ async function getUserEmail3(c) {
5381
6012
  return user?.email || "anonymous@localhost";
5382
6013
  }
5383
6014
 
6015
+ // src/shared/node-id.ts
6016
+ function assertSafeNodeId(rawId) {
6017
+ let id = rawId;
6018
+ try {
6019
+ id = decodeURIComponent(rawId);
6020
+ } catch {
6021
+ }
6022
+ if (!id || id.length > 512) {
6023
+ throw new ValidationError("invalid node id");
6024
+ }
6025
+ if (id.includes("\0") || id.includes("\\")) {
6026
+ throw new ValidationError("invalid node id");
6027
+ }
6028
+ if (id.startsWith("/")) {
6029
+ throw new ValidationError("invalid node id (absolute path)");
6030
+ }
6031
+ for (const seg of id.split("/")) {
6032
+ if (seg === "" || seg === "." || seg === "..") {
6033
+ throw new ValidationError("invalid node id (path traversal)");
6034
+ }
6035
+ }
6036
+ return id;
6037
+ }
6038
+
5384
6039
  // src/auth/anonymous.ts
5385
6040
  import bcrypt from "bcryptjs";
5386
6041
  async function ensureAnonymousUser() {
@@ -5444,7 +6099,7 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
5444
6099
  return c.json({ error: "Missing or invalid credentials" }, 401);
5445
6100
  });
5446
6101
  function createApp() {
5447
- const app = new Hono9();
6102
+ const app = new Hono10({ router: new LinearRouter() });
5448
6103
  const corsOrigins = config.CORS_ORIGINS;
5449
6104
  app.use(
5450
6105
  "*",
@@ -5465,6 +6120,22 @@ function createApp() {
5465
6120
  }
5466
6121
  return next();
5467
6122
  });
6123
+ app.use("*", async (c, next) => {
6124
+ const path = c.req.path;
6125
+ const traced = /^\/(nests|auth|admin|stats|license|index|mcp)(\/|$)/.test(path) && !path.endsWith("/mcp");
6126
+ const started = Date.now();
6127
+ await next();
6128
+ if (!traced) return;
6129
+ void logTraceEvent({
6130
+ kind: "api",
6131
+ method: c.req.method,
6132
+ path,
6133
+ nestId: /^\/nests\/([^/]+)/.exec(path)?.[1] ?? null,
6134
+ userId: c.get("userId") ?? null,
6135
+ status: c.res.status,
6136
+ durationMs: Date.now() - started
6137
+ });
6138
+ });
5468
6139
  app.get(
5469
6140
  "/health",
5470
6141
  (c) => c.json({
@@ -5564,7 +6235,8 @@ function createApp() {
5564
6235
  logo_url: config.LOGO_URL,
5565
6236
  telemetry_enabled: config.TELEMETRY_ENABLED,
5566
6237
  public_base_url: config.PUBLIC_BASE_URL,
5567
- max_body_bytes: config.MAX_BODY_BYTES
6238
+ max_body_bytes: config.MAX_BODY_BYTES,
6239
+ slack_webhook_url: config.SLACK_WEBHOOK_URL ?? ""
5568
6240
  });
5569
6241
  app.get("/admin/settings", async (c) => {
5570
6242
  if (!await adminSettingsAllowed(c))
@@ -5599,6 +6271,12 @@ function createApp() {
5599
6271
  if ("telemetry_enabled" in body) {
5600
6272
  pending.push({ name: "TELEMETRY_ENABLED", value: body.telemetry_enabled ? "true" : "false" });
5601
6273
  }
6274
+ if ("slack_webhook_url" in body) {
6275
+ const v = String(body.slack_webhook_url ?? "").trim();
6276
+ if (v && !/^https:\/\//i.test(v))
6277
+ errors.push("slack_webhook_url must be an https:// URL (or empty to disable)");
6278
+ else pending.push({ name: "SLACK_WEBHOOK_URL", value: v || null });
6279
+ }
5602
6280
  if ("max_body_bytes" in body) {
5603
6281
  const n = Number(body.max_body_bytes);
5604
6282
  if (!Number.isFinite(n) || n < 1024 * 1024 || n > 500 * 1024 * 1024)
@@ -5624,6 +6302,29 @@ function createApp() {
5624
6302
  }
5625
6303
  return c.json({ settings: currentServerSettings() });
5626
6304
  });
6305
+ app.use("/admin/trace", flexAuthMiddleware);
6306
+ const traceAllowed = async (c) => {
6307
+ if (config.AUTH_MODE === "open") return true;
6308
+ const userId = c.get("userId");
6309
+ if (await isLicenseAdminUserId(userId)) return true;
6310
+ return isSuperAdmin(await resolveCallerEmail(userId));
6311
+ };
6312
+ app.get("/admin/trace", async (c) => {
6313
+ if (!await traceAllowed(c))
6314
+ return c.json({ error: "Only the server admin can view this." }, 403);
6315
+ const limitRaw = Number(c.req.query("limit"));
6316
+ const offsetRaw = Number(c.req.query("offset"));
6317
+ const limit = Number.isFinite(limitRaw) ? limitRaw : 25;
6318
+ const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0;
6319
+ const { events, total } = await listTraceEvents({
6320
+ limit,
6321
+ offset,
6322
+ kind: c.req.query("kind"),
6323
+ nestId: c.req.query("nest") || void 0,
6324
+ user: c.req.query("user") || void 0
6325
+ });
6326
+ return c.json({ count: events.length, total, limit, offset, events });
6327
+ });
5627
6328
  app.use("/stats", flexAuthMiddleware);
5628
6329
  app.get("/stats", async (c) => {
5629
6330
  const db = getDb();
@@ -5650,7 +6351,12 @@ function createApp() {
5650
6351
  users: usersRow.c
5651
6352
  });
5652
6353
  });
5653
- const nestsApp = new Hono9();
6354
+ app.route("/llms.txt", llmsTxtRoutes);
6355
+ app.use("/index", flexAuthMiddleware);
6356
+ app.route("/index", nestIndexRoutes);
6357
+ app.use("/mcp", flexAuthMiddleware);
6358
+ app.route("/mcp", serverMcpRoutes);
6359
+ const nestsApp = new Hono10();
5654
6360
  nestsApp.use("*", flexAuthMiddleware);
5655
6361
  nestsApp.use("*", async (c, next) => {
5656
6362
  const localPath = c.req.path.replace(/^\/nests\//, "");
@@ -5759,10 +6465,14 @@ function createApp() {
5759
6465
  c.set("nestPermission", permission);
5760
6466
  return next();
5761
6467
  });
6468
+ nestsApp.use("/:nestId/nodes/:nodeId{.+}", async (c, next) => {
6469
+ assertSafeNodeId(c.req.param("nodeId"));
6470
+ return next();
6471
+ });
5762
6472
  nestsApp.route("/", nestRoutes);
5763
6473
  nestsApp.route("/:nestId", governanceRoutes);
5764
- nestsApp.route("/:nestId/nodes", governanceNodeRoutes);
5765
6474
  nestsApp.route("/:nestId/nodes", annotationRoutes);
6475
+ nestsApp.route("/:nestId/nodes", governanceNodeRoutes);
5766
6476
  nestsApp.route("/:nestId/nodes", nodeRoutes);
5767
6477
  nestsApp.route("/:nestId", queryRoutes);
5768
6478
  nestsApp.route("/:nestId", sharingRoutes);