@promptowl/contextnest-community 1.26.0 → 1.27.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 (94) hide show
  1. package/API.md +34 -1
  2. package/CONFIGURATION.md +32 -1
  3. package/dist/{chunk-TRSGPS27.js → chunk-H3W72HN3.js} +28 -0
  4. package/dist/{chunk-DGPTJXIV.js → chunk-MB52DWWP.js} +4 -4
  5. package/dist/{chunk-IEAWRHMV.js → chunk-NFFUZF2B.js} +23 -2
  6. package/dist/{chunk-PICUHQEM.js → chunk-O3JEI33N.js} +2 -2
  7. package/dist/{chunk-SWA5M5XX.js → chunk-QMQDWRHN.js} +128 -22
  8. package/dist/{chunk-I5EDEZBQ.js → chunk-RXCVXG34.js} +1 -1
  9. package/dist/{chunk-2QOJE7A5.js → chunk-SE5U4J4T.js} +4 -4
  10. package/dist/{chunk-RPGFHESR.js → chunk-VHEMGQ3I.js} +1 -1
  11. package/dist/{chunk-M6PGYMBD.js → chunk-VRLAP4HB.js} +33 -15
  12. package/dist/{engine-QNMFDIBV.js → engine-HLYQLP5T.js} +3 -3
  13. package/dist/{external-edit-service-4MEGPT4D.js → external-edit-service-3DFQ4EZZ.js} +4 -4
  14. package/dist/{grants-service-24GLUBY4.js → grants-service-WICV4Y2J.js} +2 -2
  15. package/dist/index.js +804 -304
  16. package/dist/{invited-user-AM6A3WGO.js → invited-user-XEBX6BM4.js} +3 -3
  17. package/dist/{review-service-2EWN7WWO.js → review-service-GB43UQVH.js} +7 -7
  18. package/dist/{stewardship-service-TDZUC2Q6.js → stewardship-service-KDBPWRZI.js} +4 -4
  19. package/dist/{version-service-C3JTZLJH.js → version-service-JYHFPSYP.js} +8 -4
  20. package/dist/web3/assets/{ActivityTracePage-DzwWIjjx.js → ActivityTracePage-CkRoyIc-.js} +1 -1
  21. package/dist/web3/assets/AgentDocsPage-BTRXmliv.js +1 -0
  22. package/dist/web3/assets/CollaboratorManager-CqJIkvs-.js +1 -0
  23. package/dist/web3/assets/CollaboratorsTab-JcO1P-qm.js +1 -0
  24. package/dist/web3/assets/DocumentEditor-D1GLeeEP.js +36 -0
  25. package/dist/web3/assets/DocumentsTab-BAgSxTX1.js +6 -0
  26. package/dist/web3/assets/ExternalEditsTab-DuuTXZap.js +1 -0
  27. package/dist/web3/assets/{MarkdownEditor-CKh4WSn-.js → MarkdownEditor-BELFww9q.js} +116 -126
  28. package/dist/web3/assets/{NestPageHeader-Cizo8nUi.js → NestPageHeader-Bbje2ncY.js} +1 -1
  29. package/dist/web3/assets/NestView-D4OMXPQ7.js +73 -0
  30. package/dist/web3/assets/OverviewTab-FIDGjR3Q.js +6 -0
  31. package/dist/web3/assets/{PersonCombobox-BKI7dff4.js → PersonCombobox-D52m_jhc.js} +1 -1
  32. package/dist/web3/assets/{ReviewActions-CS8JXgXF.js → ReviewActions-DYqBcWlv.js} +2 -2
  33. package/dist/web3/assets/ReviewTab-DoxfygOb.js +1 -0
  34. package/dist/web3/assets/StewardsTab-CFsYjbEn.js +1 -0
  35. package/dist/web3/assets/{SubmitForReviewModal-Biq3m79i.js → SubmitForReviewModal-AW2OLNAE.js} +1 -1
  36. package/dist/web3/assets/{alert-dialog-BvF3NFr7.js → alert-dialog-p-fBLSWx.js} +2 -2
  37. package/dist/web3/assets/{arrow-left-CWIsv-sG.js → arrow-left-CiErCCTG.js} +1 -1
  38. package/dist/web3/assets/{backlinks-Cqcn7n1J.js → backlinks-DaVSYvhV.js} +2 -2
  39. package/dist/web3/assets/{boxes-ClbgQqG2.js → boxes-VWrCCMzW.js} +1 -1
  40. package/dist/web3/assets/{chevron-left-CK_ZPFm9.js → chevron-left-CsD8gZwL.js} +1 -1
  41. package/dist/web3/assets/{circle-check-Bw_aTXiH.js → circle-check-BBCXd9LI.js} +1 -1
  42. package/dist/web3/assets/{circle-x-DkIG68H7.js → circle-x-C9LsnORQ.js} +1 -1
  43. package/dist/web3/assets/{code-xml-BlR7EN6l.js → code-xml-BaoV7HiQ.js} +1 -1
  44. package/dist/web3/assets/{corner-down-right-DSVxK3tf.js → corner-down-right-DgiTXrtw.js} +1 -1
  45. package/dist/web3/assets/{count-skeleton-DToLM74B.js → count-skeleton-CnEE4uF-.js} +1 -1
  46. package/dist/web3/assets/{earth-BXhVPvCR.js → earth-BmMbVt9k.js} +1 -1
  47. package/dist/web3/assets/{file-exclamation-point-BevGZgtj.js → file-exclamation-point-qJeca9-A.js} +1 -1
  48. package/dist/web3/assets/{folder-input-m8UBV4fA.js → folder-input-D7R93LJ6.js} +1 -1
  49. package/dist/web3/assets/index-DNmWTxN7.css +1 -0
  50. package/dist/web3/assets/index-DvuUIUP6.js +451 -0
  51. package/dist/web3/assets/{page-DzKwP5Ds.js → page-BJQuaTRW.js} +2 -2
  52. package/dist/web3/assets/{page-B6pkc9cd.js → page-Bb8xGOPQ.js} +2 -2
  53. package/dist/web3/assets/{page-Deq6IKiN.js → page-BtM-qDzr.js} +2 -2
  54. package/dist/web3/assets/{page-DPY01SOj.js → page-Bxd0EG25.js} +1 -1
  55. package/dist/web3/assets/page-C3QCO6IR.js +1 -0
  56. package/dist/web3/assets/{page-CptuAPJc.js → page-CfoR0khy.js} +2 -2
  57. package/dist/web3/assets/page-D7Zr5tAI.js +1 -0
  58. package/dist/web3/assets/{page-Dwbp4Seu.js → page-DATSlHCW.js} +2 -2
  59. package/dist/web3/assets/page-DTjPQizZ.js +45 -0
  60. package/dist/web3/assets/{page-Bgy9Ng9p.js → page-DspptlWr.js} +1 -1
  61. package/dist/web3/assets/{page-Dh57L0RG.js → page-I6vAFJOj.js} +4 -4
  62. package/dist/web3/assets/page-QxI4Ud2s.js +9 -0
  63. package/dist/web3/assets/{page-BJrFCKXB.js → page-YcNNg-dX.js} +1 -1
  64. package/dist/web3/assets/page-kM7eO5Kj.js +1 -0
  65. package/dist/web3/assets/{page-title-DVq4qvMR.js → page-title-Dn5jANiU.js} +1 -1
  66. package/dist/web3/assets/{play-CXhS6Pgq.js → play-B2k_Ix-L.js} +1 -1
  67. package/dist/web3/assets/{send-DdfwnIrj.js → send-DPNxv3As.js} +1 -1
  68. package/dist/web3/assets/{settings-DKX0Q4gO.js → settings-DZjq-OQA.js} +1 -1
  69. package/dist/web3/assets/{share-2-CxmXRt58.js → share-2-C-OnE2ru.js} +1 -1
  70. package/dist/web3/assets/{tag-DeYC_mRv.js → tag-CwJl7FdI.js} +1 -1
  71. package/dist/web3/assets/{trash-2-B0HyPTnI.js → trash-2-aljmIff4.js} +1 -1
  72. package/dist/web3/assets/{user-plus-Ckm_Isyc.js → user-plus-DPerZexX.js} +1 -1
  73. package/dist/web3/assets/{zap-D2ifcnYb.js → zap-CHZtiN7P.js} +1 -1
  74. package/dist/web3/index.html +2 -2
  75. package/package.json +6 -2
  76. package/dist/web3/assets/AgentDocsPage-ixP2UdAE.js +0 -1
  77. package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +0 -1
  78. package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +0 -1
  79. package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +0 -36
  80. package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +0 -6
  81. package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +0 -1
  82. package/dist/web3/assets/NestView-EW-sraEb.js +0 -73
  83. package/dist/web3/assets/OverviewTab-CjidsMX_.js +0 -6
  84. package/dist/web3/assets/ReviewTab-CwVrXzw5.js +0 -1
  85. package/dist/web3/assets/StewardsTab-C-iP6gnh.js +0 -1
  86. package/dist/web3/assets/index-BOVYgMvj.js +0 -29
  87. package/dist/web3/assets/index-DVnaYqj9.css +0 -1
  88. package/dist/web3/assets/index-WYDmEOfO.js +0 -404
  89. package/dist/web3/assets/page-C2zoxumw.js +0 -1
  90. package/dist/web3/assets/page-CJWO06dN.js +0 -1
  91. package/dist/web3/assets/page-Dea2SblF.js +0 -9
  92. package/dist/web3/assets/page-Dfjg8p7Z.js +0 -1
  93. package/dist/web3/assets/page-YxjrE5b5.js +0 -45
  94. package/dist/web3/assets/x-mNtADYHc.js +0 -6
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  submitForReview,
39
39
  withNodeWriteLock,
40
40
  withdrawDeletionRequest
41
- } from "./chunk-M6PGYMBD.js";
41
+ } from "./chunk-VRLAP4HB.js";
42
42
  import {
43
43
  canCreateInNest,
44
44
  canManageStewards,
@@ -59,7 +59,7 @@ import {
59
59
  stewardDocumentCreator,
60
60
  syncFromConfig,
61
61
  updateSteward
62
- } from "./chunk-DGPTJXIV.js";
62
+ } from "./chunk-MB52DWWP.js";
63
63
  import {
64
64
  createGrant,
65
65
  deleteGrant,
@@ -68,7 +68,7 @@ import {
68
68
  listGrants,
69
69
  listUserGrants,
70
70
  resolveNodeGrant
71
- } from "./chunk-RPGFHESR.js";
71
+ } from "./chunk-VHEMGQ3I.js";
72
72
  import {
73
73
  attributeRequest,
74
74
  checkConflict,
@@ -88,11 +88,12 @@ import {
88
88
  getVersions,
89
89
  listMyDrafts,
90
90
  mergeClient,
91
+ moveApprovedVersion,
91
92
  parseClientMetadata,
92
93
  serializeClient,
93
94
  setApprovedVersion,
94
95
  upsertVersion
95
- } from "./chunk-IEAWRHMV.js";
96
+ } from "./chunk-NFFUZF2B.js";
96
97
  import {
97
98
  approveExternalEdit,
98
99
  getExternalEditDetail,
@@ -104,7 +105,7 @@ import {
104
105
  scanDocumentForDrift,
105
106
  scanNestForDrift,
106
107
  startDriftScanner
107
- } from "./chunk-2QOJE7A5.js";
108
+ } from "./chunk-SE5U4J4T.js";
108
109
  import {
109
110
  addMember,
110
111
  addWatcher,
@@ -205,7 +206,7 @@ import {
205
206
  unshareTeamFromNest,
206
207
  updateMemberRole,
207
208
  validateLicense
208
- } from "./chunk-SWA5M5XX.js";
209
+ } from "./chunk-QMQDWRHN.js";
209
210
  import {
210
211
  AppError,
211
212
  ConflictError,
@@ -228,7 +229,7 @@ import {
228
229
  parseBearerToken,
229
230
  verifyKeyRequest,
230
231
  verifyPassword
231
- } from "./chunk-PICUHQEM.js";
232
+ } from "./chunk-O3JEI33N.js";
232
233
  import {
233
234
  canApproveWith,
234
235
  canEditWith,
@@ -238,14 +239,14 @@ import {
238
239
  sendEmailToRecipient,
239
240
  transportSettings,
240
241
  verifyEmailTransport
241
- } from "./chunk-I5EDEZBQ.js";
242
+ } from "./chunk-RXCVXG34.js";
242
243
  import {
243
244
  config,
244
245
  getDb,
245
246
  initDb,
246
247
  isEmailListish,
247
248
  isEmailish
248
- } from "./chunk-TRSGPS27.js";
249
+ } from "./chunk-H3W72HN3.js";
249
250
  import {
250
251
  ANON_EMAIL,
251
252
  ANON_USER_ID,
@@ -259,7 +260,7 @@ import {
259
260
  import { serve } from "@hono/node-server";
260
261
 
261
262
  // src/app.ts
262
- import { Hono as Hono27 } from "hono";
263
+ import { Hono as Hono28 } from "hono";
263
264
  import { LinearRouter } from "hono/router/linear-router";
264
265
  import { createMiddleware as createMiddleware3 } from "hono/factory";
265
266
  import { cors } from "hono/cors";
@@ -802,9 +803,9 @@ function isOidcReady() {
802
803
  }
803
804
  async function discover(issuer) {
804
805
  const now = Date.now();
805
- const cached = discoveryCache.get(issuer);
806
- if (cached && now - cached.fetchedAt < DISCOVERY_TTL_MS) {
807
- return { doc: cached.doc, jwks: cached.jwks };
806
+ const cached2 = discoveryCache.get(issuer);
807
+ if (cached2 && now - cached2.fetchedAt < DISCOVERY_TTL_MS) {
808
+ return { doc: cached2.doc, jwks: cached2.jwks };
808
809
  }
809
810
  const res = await fetch(`${issuer}/.well-known/openid-configuration`, {
810
811
  headers: { Accept: "application/json" },
@@ -2164,6 +2165,35 @@ function stripUndefinedDeep(value) {
2164
2165
  // src/nodes/service.ts
2165
2166
  import { inputJsonSchema } from "@promptowl/contextnest-engine/api";
2166
2167
 
2168
+ // src/governance/approved-body.ts
2169
+ async function reconstructApproved(versionManager, nestId, nodeId, approved) {
2170
+ try {
2171
+ return { raw: await versionManager.reconstructVersion(nodeId, approved), version: approved };
2172
+ } catch (err) {
2173
+ const current = await getApprovedVersion(nestId, nodeId);
2174
+ if (current != null && current !== approved) {
2175
+ return { raw: await versionManager.reconstructVersion(nodeId, current), version: current };
2176
+ }
2177
+ const newer = (await getVersions(nestId, nodeId)).filter(
2178
+ (v) => v.version > approved && (v.status === "published" || v.status === "approved")
2179
+ );
2180
+ for (const { version } of newer) {
2181
+ let raw;
2182
+ try {
2183
+ raw = await versionManager.reconstructVersion(nodeId, version);
2184
+ } catch {
2185
+ continue;
2186
+ }
2187
+ if (await moveApprovedVersion(nestId, nodeId, approved, version)) {
2188
+ console.warn(`[approved-heal] ${nestId}/${nodeId}: approved v${approved} \u2192 v${version} (v${approved} could not be rebuilt)`);
2189
+ }
2190
+ return { raw, version };
2191
+ }
2192
+ console.error(`[approved-heal] ${nestId}/${nodeId}: v${approved} can't be rebuilt and no newer published version can`, err);
2193
+ throw err;
2194
+ }
2195
+ }
2196
+
2167
2197
  // src/governance/prime-service.ts
2168
2198
  async function isPrimeDocument(nestId, tags, metadata) {
2169
2199
  const flag = documentPrimeFlag(metadata);
@@ -2204,6 +2234,21 @@ function extractMentions(markdown2) {
2204
2234
  }
2205
2235
  return names;
2206
2236
  }
2237
+ function stripMentions(text) {
2238
+ const spans = [];
2239
+ const code = (text || "").replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length)).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length));
2240
+ let match;
2241
+ MENTION.lastIndex = 0;
2242
+ while ((match = MENTION.exec(code)) !== null) {
2243
+ const lead = match[0].startsWith("@") ? 0 : 1;
2244
+ spans.push([match.index + lead, match.index + match[0].length]);
2245
+ }
2246
+ let out = text || "";
2247
+ for (const [start, end] of spans.reverse()) {
2248
+ out = out.slice(0, start) + out.slice(end);
2249
+ }
2250
+ return out.replace(/\s+/g, " ").trim();
2251
+ }
2207
2252
  function newMentions(previous, next) {
2208
2253
  const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
2209
2254
  return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
@@ -2973,13 +3018,16 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
2973
3018
  r.myGrant = grantRoleForNode(myGrants, doc.id);
2974
3019
  }
2975
3020
  if (approvedVersions) {
2976
- const approved = approvedVersions.get(doc.id);
3021
+ let approved = approvedVersions.get(doc.id);
2977
3022
  if (approved != null) {
2978
3023
  try {
2979
- const raw = await versionManager.reconstructVersion(
3024
+ let raw;
3025
+ ({ raw, version: approved } = await reconstructApproved(
3026
+ versionManager,
3027
+ nestId,
2980
3028
  doc.id,
2981
3029
  approved
2982
- );
3030
+ ));
2983
3031
  r.content = bodyOnly(doc.id, raw);
2984
3032
  } catch (err) {
2985
3033
  console.error(
@@ -5281,7 +5329,7 @@ async function requireManage(c, nestId, opts = {}) {
5281
5329
  }
5282
5330
  }
5283
5331
  async function resolveGrantee(email) {
5284
- const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-AM6A3WGO.js");
5332
+ const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-XEBX6BM4.js");
5285
5333
  return (await findOrCreateInvitedUser2(email)).id;
5286
5334
  }
5287
5335
  async function assertGrantAddsAccess(params) {
@@ -7017,7 +7065,7 @@ nodeRoutes.post("/", async (c) => {
7017
7065
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
7018
7066
  const nestId = c.req.param("nestId");
7019
7067
  const nodeId = c.req.param("nodeId");
7020
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-TDZUC2Q6.js");
7068
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-KDBPWRZI.js");
7021
7069
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
7022
7070
  nestId,
7023
7071
  nodeId
@@ -7038,7 +7086,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
7038
7086
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
7039
7087
  const nestId = c.req.param("nestId");
7040
7088
  const nodeId = c.req.param("nodeId");
7041
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-2EWN7WWO.js");
7089
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-GB43UQVH.js");
7042
7090
  const history = await getReviewHistory2(nestId, nodeId);
7043
7091
  return c.json({ reviews: history });
7044
7092
  });
@@ -7194,14 +7242,16 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
7194
7242
  response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
7195
7243
  const approvedOnly = c.req.query("approved_only") === "1" || c.req.query("approved_only") === "true";
7196
7244
  if (publicReader || approvedOnly) {
7197
- const approved = await getApprovedVersion(nestId, nodeId);
7245
+ let approved = await getApprovedVersion(nestId, nodeId);
7198
7246
  if (approved != null) {
7199
7247
  let approvedRaw = "";
7200
7248
  try {
7201
- approvedRaw = await versionManager.reconstructVersion(
7249
+ ({ raw: approvedRaw, version: approved } = await reconstructApproved(
7250
+ versionManager,
7251
+ nestId,
7202
7252
  nodeId,
7203
7253
  approved
7204
- );
7254
+ ));
7205
7255
  response.content = bodyOnly(nodeId, approvedRaw);
7206
7256
  if (response.type === "pdf") {
7207
7257
  const approvedPdf = pdfMetaOf(nodeId, approvedRaw);
@@ -7890,7 +7940,7 @@ async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
7890
7940
  if (approved == null) return "";
7891
7941
  try {
7892
7942
  const { versions } = await engineCache.get(nestId);
7893
- const raw = await versions.reconstructVersion(nodeId, approved);
7943
+ const { raw } = await reconstructApproved(versions, nestId, nodeId, approved);
7894
7944
  return bodyOnly(nodeId, raw);
7895
7945
  } catch {
7896
7946
  return "";
@@ -7902,75 +7952,13 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
7902
7952
  if (approved == null) return null;
7903
7953
  try {
7904
7954
  const { versions } = await engineCache.get(nestId);
7905
- const raw = await versions.reconstructVersion(nodeId, approved);
7955
+ const { raw } = await reconstructApproved(versions, nestId, nodeId, approved);
7906
7956
  return bodyOnly(nodeId, raw);
7907
7957
  } catch {
7908
7958
  return null;
7909
7959
  }
7910
7960
  }
7911
7961
 
7912
- // src/nests/bundle-service.ts
7913
- import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
7914
- import { join as join5, sep } from "path";
7915
- import JSZip from "jszip";
7916
- import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
7917
- function walkFiles(root) {
7918
- const out = [];
7919
- let entries;
7920
- try {
7921
- entries = readdirSync2(root, { recursive: true });
7922
- } catch {
7923
- return out;
7924
- }
7925
- for (const rel of entries) {
7926
- try {
7927
- if (statSync2(join5(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
7928
- } catch {
7929
- }
7930
- }
7931
- return out;
7932
- }
7933
- async function exportNestBundle(nestId, includeDrafts) {
7934
- const db = getDb();
7935
- const approvedRows = await db.all(
7936
- "SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
7937
- [nestId]
7938
- );
7939
- const approvedByNode = new Map(
7940
- approvedRows.map((r) => [r.node_id, r.approved_version])
7941
- );
7942
- const { storage } = await engineCache.get(nestId);
7943
- const docs = await storage.discoverDocuments();
7944
- const zip = new JSZip();
7945
- let count = 0;
7946
- for (const doc of docs) {
7947
- const approved = approvedByNode.get(doc.id);
7948
- const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
7949
- let body = doc.body || "";
7950
- if (approved != null) {
7951
- const approvedBody = await resolveExportBody(nestId, doc.id, body);
7952
- if (approvedBody == null) continue;
7953
- body = approvedBody;
7954
- frontmatter.status = "published";
7955
- frontmatter.version = approved;
7956
- } else {
7957
- if (!includeDrafts) continue;
7958
- frontmatter.status = "draft";
7959
- }
7960
- zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
7961
- count++;
7962
- }
7963
- const nestPath = resolveNestPath(nestId);
7964
- for (const rel of walkFiles(nestPath)) {
7965
- const segs = rel.split("/");
7966
- const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
7967
- if (!keep) continue;
7968
- zip.file(rel, readFileSync2(join5(nestPath, rel)));
7969
- }
7970
- trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
7971
- return zip.generateAsync({ type: "arraybuffer" });
7972
- }
7973
-
7974
7962
  // src/nodes/graph-service.ts
7975
7963
  import {
7976
7964
  contextLinkTarget,
@@ -8156,132 +8144,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
8156
8144
  };
8157
8145
  }
8158
8146
 
8159
- // src/nodes/query-routes.ts
8160
- var queryRoutes = new Hono14();
8161
- queryRoutes.get("/changes", async (c) => {
8162
- const nestId = c.req.param("nestId");
8163
- const userId = c.get("userId");
8164
- const sinceRaw = c.req.query("since");
8165
- const sinceMs = sinceRaw ? Date.parse(sinceRaw) : NaN;
8166
- if (!sinceRaw || Number.isNaN(sinceMs)) {
8167
- throw new ValidationError(
8168
- "since must be an ISO-8601 date, e.g. ?since=2026-07-01T00:00:00Z"
8169
- );
8170
- }
8171
- const includeContent = ["1", "true"].includes(
8172
- c.req.query("include_content") || ""
8173
- );
8174
- const all = await listNodesForCaller(nestId, userId, {
8175
- ...includeContent ? { withContent: true } : {}
8176
- });
8177
- const db = getDb();
8178
- const publicReader = await isPublicReader(nestId, userId);
8179
- const approvedRows = await db.all(
8180
- "SELECT node_id, approved_at FROM approved_versions WHERE nest_id = ?",
8181
- [nestId]
8182
- );
8183
- const parseDbTs = (t) => Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`);
8184
- const approvedAtMsByNode = new Map(
8185
- approvedRows.map((r) => [r.node_id, parseDbTs(r.approved_at)])
8186
- );
8187
- const publishedSince = new Set(
8188
- approvedRows.filter((r) => parseDbTs(r.approved_at) > sinceMs).map((r) => r.node_id)
8189
- );
8190
- const summarize = (n, updatedOverride) => ({
8191
- id: n.id,
8192
- title: n.title,
8193
- type: n.type,
8194
- status: n.status,
8195
- tags: n.tags,
8196
- created_at: n.created_at,
8197
- updated_at: updatedOverride ?? n.updated_at,
8198
- ...includeContent ? { content: n.content } : {}
8199
- });
8200
- const created = [];
8201
- const updated = [];
8202
- for (const n of all) {
8203
- if (publicReader) {
8204
- const approvedMs = approvedAtMsByNode.get(n.id);
8205
- if (approvedMs === void 0) continue;
8206
- const approvedIso = new Date(approvedMs).toISOString();
8207
- const createdAt2 = Date.parse(n.created_at || "");
8208
- if (!Number.isNaN(createdAt2) && createdAt2 > sinceMs) {
8209
- created.push(summarize(n, approvedIso));
8210
- } else if (approvedMs > sinceMs) {
8211
- updated.push(summarize(n, approvedIso));
8212
- }
8213
- continue;
8214
- }
8215
- const createdAt = Date.parse(n.created_at || "");
8216
- const updatedAt = Date.parse(n.updated_at || "");
8217
- if (!Number.isNaN(createdAt) && createdAt > sinceMs) created.push(summarize(n));
8218
- else if (!Number.isNaN(updatedAt) && updatedAt > sinceMs) updated.push(summarize(n));
8219
- else if (publishedSince.has(n.id)) updated.push(summarize(n));
8220
- }
8221
- const canSeeDeleted = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
8222
- let deleted = null;
8223
- if (canSeeDeleted) {
8224
- const rows = await db.all(
8225
- "SELECT node_id, deleted_by, deleted_at FROM node_deletions WHERE nest_id = ? AND deleted_at > ?",
8226
- [nestId, new Date(sinceMs).toISOString()]
8227
- );
8228
- const liveIds = new Set(all.map((n) => n.id));
8229
- 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 }));
8230
- }
8231
- return c.json({
8232
- since: new Date(sinceMs).toISOString(),
8233
- now: (/* @__PURE__ */ new Date()).toISOString(),
8234
- created,
8235
- updated,
8236
- deleted
8237
- });
8238
- });
8239
- queryRoutes.get("/runnables", async (c) => {
8240
- const nestId = c.req.param("nestId");
8241
- const userId = c.get("userId");
8242
- const all = await listNodesForCaller(nestId, userId, {
8243
- type: RUNNABLE_NODE_TYPES,
8244
- // The prompt/skill body IS the payload here — an orchestrator pulls it to
8245
- // run the thing, so this surface pays for the file reads.
8246
- withContent: true
8247
- });
8248
- const runnables = all.map((n) => ({
8249
- id: n.id,
8250
- title: n.title,
8251
- type: n.type,
8252
- schedule: n.schedule ?? null,
8253
- status: n.status,
8254
- tags: n.tags,
8255
- updated_at: n.updated_at,
8256
- content: n.content
8257
- }));
8258
- return c.json({ count: runnables.length, runnables });
8259
- });
8260
- queryRoutes.get("/graph", async (c) => {
8261
- const nestId = c.req.param("nestId");
8262
- const userId = c.get("userId");
8263
- const userEmail = await resolveCallerEmail(userId);
8264
- const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
8265
- const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
8266
- if (config.FEATURE_WORKFLOW_PLANE) {
8267
- const rows = await getDb().all(
8268
- `SELECT e.from_node, e.to_node, e.condition_mode, t.name as type, t.color, t.is_flow
8269
- FROM edges e JOIN edge_types t ON t.id = e.type_id
8270
- WHERE e.nest_id = ?`,
8271
- [nestId]
8272
- );
8273
- const visible = new Set(graph.nodes.map((n) => n.id));
8274
- graph.typedEdges = rows.filter((r) => visible.has(r.from_node) && visible.has(r.to_node)).map((r) => ({
8275
- source: r.from_node,
8276
- target: r.to_node,
8277
- type: r.type,
8278
- color: r.color,
8279
- is_flow: !!r.is_flow,
8280
- condition_mode: r.condition_mode
8281
- }));
8282
- }
8283
- return c.json(graph);
8284
- });
8147
+ // src/nodes/context-service.ts
8285
8148
  function approxTokens(text) {
8286
8149
  return Math.ceil(text.length / 4);
8287
8150
  }
@@ -8300,13 +8163,11 @@ ${meta.join("\n")}
8300
8163
  `;
8301
8164
  return header + (node.body || "").trim();
8302
8165
  }
8303
- queryRoutes.post("/context", async (c) => {
8304
- const body = await c.req.json();
8166
+ async function resolveContext(params) {
8167
+ const { nestId, userId, baseUrl, body } = params;
8305
8168
  if (!body.prompt && !body.selector) {
8306
8169
  throw new ValidationError("prompt or selector is required");
8307
8170
  }
8308
- const nestId = c.req.param("nestId");
8309
- const baseUrl = requestBaseUrl(c.req.url);
8310
8171
  const { query: queryEngine, storage } = await engineCache.get(nestId);
8311
8172
  const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
8312
8173
  const promptPath = !body.selector?.trim() && !!body.prompt;
@@ -8321,17 +8182,23 @@ queryRoutes.post("/context", async (c) => {
8321
8182
  if (selector && selector.includes("[[")) {
8322
8183
  const parts = selector.split("|").map((p) => p.trim()).filter(Boolean);
8323
8184
  const wantedTitles = /* @__PURE__ */ new Set();
8185
+ const wantedIds = /* @__PURE__ */ new Set();
8324
8186
  const rest = [];
8325
8187
  for (const p of parts) {
8326
8188
  const m = /^\[\[(.+)\]\]$/.exec(p);
8327
- if (m) wantedTitles.add(m[1].trim().toLowerCase());
8328
- else rest.push(p);
8189
+ if (!m) {
8190
+ rest.push(p);
8191
+ continue;
8192
+ }
8193
+ const inner = m[1].trim();
8194
+ if (inner.startsWith("nodes/")) wantedIds.add(inner);
8195
+ else wantedTitles.add(inner.toLowerCase());
8329
8196
  }
8330
- if (wantedTitles.size > 0) {
8197
+ if (wantedTitles.size > 0 || wantedIds.size > 0) {
8331
8198
  selector = rest.join("|") || null;
8332
8199
  resolvedTitleSelector = true;
8333
8200
  titleMatches = allDocs.filter(
8334
- (d) => wantedTitles.has(String(d.frontmatter.title || "").toLowerCase()) && isVisible(d)
8201
+ (d) => (wantedIds.has(d.id) || wantedTitles.has(String(d.frontmatter.title || "").toLowerCase())) && isVisible(d)
8335
8202
  );
8336
8203
  }
8337
8204
  }
@@ -8436,7 +8303,6 @@ queryRoutes.post("/context", async (c) => {
8436
8303
  });
8437
8304
  documents.sort((a, b) => relevance.get(b.id).score - relevance.get(a.id).score);
8438
8305
  }
8439
- const userId = c.get("userId");
8440
8306
  const userEmail = await resolveCallerEmail(userId);
8441
8307
  const beforePermission = documents.length;
8442
8308
  const accessible = await filterAccessible(nestId, userId, userEmail, documents);
@@ -8470,8 +8336,13 @@ queryRoutes.post("/context", async (c) => {
8470
8336
  permission_filtered: permissionFiltered,
8471
8337
  token_count: tokenCount
8472
8338
  });
8473
- return c.json({
8339
+ return {
8474
8340
  context: contextMarkdown,
8341
+ blocks: included.map((n) => ({
8342
+ id: n.id,
8343
+ title: n.frontmatter.title,
8344
+ markdown: formatContextBlock(n, n.url)
8345
+ })),
8475
8346
  nodes: included.map((n) => ({
8476
8347
  id: n.id,
8477
8348
  title: n.frontmatter.title,
@@ -8498,37 +8369,236 @@ queryRoutes.post("/context", async (c) => {
8498
8369
  approx_tokens: tokenCount,
8499
8370
  max_tokens: maxTokens
8500
8371
  }
8501
- });
8502
- });
8503
- queryRoutes.post("/table-query", async (c) => {
8504
- const nestId = c.req.param("nestId");
8505
- const body = await c.req.json();
8506
- const ref = typeof body.table === "string" ? body.table.trim() : "";
8507
- if (!ref) throw new ValidationError("table is required \u2014 [[Title]] or node id");
8508
- const result = await queryTableNode(
8509
- nestId,
8510
- ref,
8511
- typeof body.query === "string" ? body.query : "",
8512
- body.include_drafts === true
8513
- );
8514
- return c.json(result);
8515
- });
8516
- queryRoutes.post("/query", async (c) => {
8517
- const body = await c.req.json();
8518
- if (!body.query) {
8519
- throw new ValidationError("query is required");
8520
- }
8521
- const nestId = c.req.param("nestId");
8522
- const { query: queryEngine } = await engineCache.get(nestId);
8523
- const result = await queryEngine.query(body.query, {
8524
- hops: body.hops ?? 2
8525
- });
8526
- const userId = c.get("userId");
8527
- const userEmail = await resolveCallerEmail(userId);
8528
- const accessible = await filterAccessible(nestId, userId, userEmail, result.documents);
8529
- return c.json({
8530
- query: body.query,
8531
- count: accessible.length,
8372
+ };
8373
+ }
8374
+
8375
+ // src/nests/bundle-service.ts
8376
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
8377
+ import { join as join5, sep } from "path";
8378
+ import JSZip from "jszip";
8379
+ import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
8380
+ function walkFiles(root) {
8381
+ const out = [];
8382
+ let entries;
8383
+ try {
8384
+ entries = readdirSync2(root, { recursive: true });
8385
+ } catch {
8386
+ return out;
8387
+ }
8388
+ for (const rel of entries) {
8389
+ try {
8390
+ if (statSync2(join5(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
8391
+ } catch {
8392
+ }
8393
+ }
8394
+ return out;
8395
+ }
8396
+ async function exportNestBundle(nestId, includeDrafts) {
8397
+ const db = getDb();
8398
+ const approvedRows = await db.all(
8399
+ "SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
8400
+ [nestId]
8401
+ );
8402
+ const approvedByNode = new Map(
8403
+ approvedRows.map((r) => [r.node_id, r.approved_version])
8404
+ );
8405
+ const { storage } = await engineCache.get(nestId);
8406
+ const docs = await storage.discoverDocuments();
8407
+ const zip = new JSZip();
8408
+ let count = 0;
8409
+ for (const doc of docs) {
8410
+ const approved = approvedByNode.get(doc.id);
8411
+ const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
8412
+ let body = doc.body || "";
8413
+ if (approved != null) {
8414
+ const approvedBody = await resolveExportBody(nestId, doc.id, body);
8415
+ if (approvedBody == null) continue;
8416
+ body = approvedBody;
8417
+ frontmatter.status = "published";
8418
+ frontmatter.version = approved;
8419
+ } else {
8420
+ if (!includeDrafts) continue;
8421
+ frontmatter.status = "draft";
8422
+ }
8423
+ zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
8424
+ count++;
8425
+ }
8426
+ const nestPath = resolveNestPath(nestId);
8427
+ for (const rel of walkFiles(nestPath)) {
8428
+ const segs = rel.split("/");
8429
+ const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
8430
+ if (!keep) continue;
8431
+ zip.file(rel, readFileSync2(join5(nestPath, rel)));
8432
+ }
8433
+ trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
8434
+ return zip.generateAsync({ type: "arraybuffer" });
8435
+ }
8436
+
8437
+ // src/nodes/query-routes.ts
8438
+ var queryRoutes = new Hono14();
8439
+ queryRoutes.get("/changes", async (c) => {
8440
+ const nestId = c.req.param("nestId");
8441
+ const userId = c.get("userId");
8442
+ const sinceRaw = c.req.query("since");
8443
+ const sinceMs = sinceRaw ? Date.parse(sinceRaw) : NaN;
8444
+ if (!sinceRaw || Number.isNaN(sinceMs)) {
8445
+ throw new ValidationError(
8446
+ "since must be an ISO-8601 date, e.g. ?since=2026-07-01T00:00:00Z"
8447
+ );
8448
+ }
8449
+ const includeContent = ["1", "true"].includes(
8450
+ c.req.query("include_content") || ""
8451
+ );
8452
+ const all = await listNodesForCaller(nestId, userId, {
8453
+ ...includeContent ? { withContent: true } : {}
8454
+ });
8455
+ const db = getDb();
8456
+ const publicReader = await isPublicReader(nestId, userId);
8457
+ const approvedRows = await db.all(
8458
+ "SELECT node_id, approved_at FROM approved_versions WHERE nest_id = ?",
8459
+ [nestId]
8460
+ );
8461
+ const parseDbTs = (t) => Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`);
8462
+ const approvedAtMsByNode = new Map(
8463
+ approvedRows.map((r) => [r.node_id, parseDbTs(r.approved_at)])
8464
+ );
8465
+ const publishedSince = new Set(
8466
+ approvedRows.filter((r) => parseDbTs(r.approved_at) > sinceMs).map((r) => r.node_id)
8467
+ );
8468
+ const summarize = (n, updatedOverride) => ({
8469
+ id: n.id,
8470
+ title: n.title,
8471
+ type: n.type,
8472
+ status: n.status,
8473
+ tags: n.tags,
8474
+ created_at: n.created_at,
8475
+ updated_at: updatedOverride ?? n.updated_at,
8476
+ ...includeContent ? { content: n.content } : {}
8477
+ });
8478
+ const created = [];
8479
+ const updated = [];
8480
+ for (const n of all) {
8481
+ if (publicReader) {
8482
+ const approvedMs = approvedAtMsByNode.get(n.id);
8483
+ if (approvedMs === void 0) continue;
8484
+ const approvedIso = new Date(approvedMs).toISOString();
8485
+ const createdAt2 = Date.parse(n.created_at || "");
8486
+ if (!Number.isNaN(createdAt2) && createdAt2 > sinceMs) {
8487
+ created.push(summarize(n, approvedIso));
8488
+ } else if (approvedMs > sinceMs) {
8489
+ updated.push(summarize(n, approvedIso));
8490
+ }
8491
+ continue;
8492
+ }
8493
+ const createdAt = Date.parse(n.created_at || "");
8494
+ const updatedAt = Date.parse(n.updated_at || "");
8495
+ if (!Number.isNaN(createdAt) && createdAt > sinceMs) created.push(summarize(n));
8496
+ else if (!Number.isNaN(updatedAt) && updatedAt > sinceMs) updated.push(summarize(n));
8497
+ else if (publishedSince.has(n.id)) updated.push(summarize(n));
8498
+ }
8499
+ const canSeeDeleted = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
8500
+ let deleted = null;
8501
+ if (canSeeDeleted) {
8502
+ const rows = await db.all(
8503
+ "SELECT node_id, deleted_by, deleted_at FROM node_deletions WHERE nest_id = ? AND deleted_at > ?",
8504
+ [nestId, new Date(sinceMs).toISOString()]
8505
+ );
8506
+ const liveIds = new Set(all.map((n) => n.id));
8507
+ 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 }));
8508
+ }
8509
+ return c.json({
8510
+ since: new Date(sinceMs).toISOString(),
8511
+ now: (/* @__PURE__ */ new Date()).toISOString(),
8512
+ created,
8513
+ updated,
8514
+ deleted
8515
+ });
8516
+ });
8517
+ queryRoutes.get("/runnables", async (c) => {
8518
+ const nestId = c.req.param("nestId");
8519
+ const userId = c.get("userId");
8520
+ const all = await listNodesForCaller(nestId, userId, {
8521
+ type: RUNNABLE_NODE_TYPES,
8522
+ // The prompt/skill body IS the payload here — an orchestrator pulls it to
8523
+ // run the thing, so this surface pays for the file reads.
8524
+ withContent: true
8525
+ });
8526
+ const runnables = all.map((n) => ({
8527
+ id: n.id,
8528
+ title: n.title,
8529
+ type: n.type,
8530
+ schedule: n.schedule ?? null,
8531
+ status: n.status,
8532
+ tags: n.tags,
8533
+ updated_at: n.updated_at,
8534
+ content: n.content
8535
+ }));
8536
+ return c.json({ count: runnables.length, runnables });
8537
+ });
8538
+ queryRoutes.get("/graph", async (c) => {
8539
+ const nestId = c.req.param("nestId");
8540
+ const userId = c.get("userId");
8541
+ const userEmail = await resolveCallerEmail(userId);
8542
+ const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
8543
+ const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
8544
+ if (config.FEATURE_WORKFLOW_PLANE) {
8545
+ const rows = await getDb().all(
8546
+ `SELECT e.from_node, e.to_node, e.condition_mode, t.name as type, t.color, t.is_flow
8547
+ FROM edges e JOIN edge_types t ON t.id = e.type_id
8548
+ WHERE e.nest_id = ?`,
8549
+ [nestId]
8550
+ );
8551
+ const visible = new Set(graph.nodes.map((n) => n.id));
8552
+ graph.typedEdges = rows.filter((r) => visible.has(r.from_node) && visible.has(r.to_node)).map((r) => ({
8553
+ source: r.from_node,
8554
+ target: r.to_node,
8555
+ type: r.type,
8556
+ color: r.color,
8557
+ is_flow: !!r.is_flow,
8558
+ condition_mode: r.condition_mode
8559
+ }));
8560
+ }
8561
+ return c.json(graph);
8562
+ });
8563
+ queryRoutes.post("/context", async (c) => {
8564
+ const body = await c.req.json();
8565
+ const { blocks: _blocks, ...result } = await resolveContext({
8566
+ nestId: c.req.param("nestId"),
8567
+ userId: c.get("userId"),
8568
+ baseUrl: requestBaseUrl(c.req.url),
8569
+ body
8570
+ });
8571
+ return c.json(result);
8572
+ });
8573
+ queryRoutes.post("/table-query", async (c) => {
8574
+ const nestId = c.req.param("nestId");
8575
+ const body = await c.req.json();
8576
+ const ref = typeof body.table === "string" ? body.table.trim() : "";
8577
+ if (!ref) throw new ValidationError("table is required \u2014 [[Title]] or node id");
8578
+ const result = await queryTableNode(
8579
+ nestId,
8580
+ ref,
8581
+ typeof body.query === "string" ? body.query : "",
8582
+ body.include_drafts === true
8583
+ );
8584
+ return c.json(result);
8585
+ });
8586
+ queryRoutes.post("/query", async (c) => {
8587
+ const body = await c.req.json();
8588
+ if (!body.query) {
8589
+ throw new ValidationError("query is required");
8590
+ }
8591
+ const nestId = c.req.param("nestId");
8592
+ const { query: queryEngine } = await engineCache.get(nestId);
8593
+ const result = await queryEngine.query(body.query, {
8594
+ hops: body.hops ?? 2
8595
+ });
8596
+ const userId = c.get("userId");
8597
+ const userEmail = await resolveCallerEmail(userId);
8598
+ const accessible = await filterAccessible(nestId, userId, userEmail, result.documents);
8599
+ return c.json({
8600
+ query: body.query,
8601
+ count: accessible.length,
8532
8602
  nodes: accessible.map((n) => ({
8533
8603
  id: n.id,
8534
8604
  title: n.frontmatter.title,
@@ -8754,8 +8824,393 @@ queryRoutes.post("/publish", async (c) => {
8754
8824
  );
8755
8825
  });
8756
8826
 
8827
+ // src/hootie/routes.ts
8828
+ import { Hono as Hono15 } from "hono";
8829
+ import { streamSSE } from "hono/streaming";
8830
+
8831
+ // src/hootie/model.ts
8832
+ import Anthropic from "@anthropic-ai/sdk";
8833
+ var plainModels = /* @__PURE__ */ new Set();
8834
+ async function* streamAnswer(params) {
8835
+ const client = clientFor(params.apiKey, params.baseUrl ?? null);
8836
+ const key = `${params.baseUrl ?? ""}|${params.model}`;
8837
+ if (plainModels.has(key)) {
8838
+ yield* answer(client, params, false);
8839
+ return;
8840
+ }
8841
+ const state = { started: false };
8842
+ try {
8843
+ yield* answer(client, params, true, state);
8844
+ } catch (err) {
8845
+ if (state.started || err?.status !== 400) throw err;
8846
+ yield* answer(client, params, false);
8847
+ plainModels.add(key);
8848
+ }
8849
+ }
8850
+ async function* answer(client, params, tuned, state = { started: false }) {
8851
+ const stream = client.messages.stream(
8852
+ tuned ? {
8853
+ model: params.model,
8854
+ max_tokens: 4096,
8855
+ // The system prompt carries the retrieved context — the bulk of the
8856
+ // request, identical across a conversation's turns — so cache it.
8857
+ system: [
8858
+ { type: "text", text: params.system, cache_control: { type: "ephemeral" } }
8859
+ ],
8860
+ messages: params.turns,
8861
+ thinking: { type: "adaptive" },
8862
+ output_config: { effort: "medium" }
8863
+ } : (
8864
+ // The bare request every Messages-speaking model accepts.
8865
+ {
8866
+ model: params.model,
8867
+ max_tokens: 4096,
8868
+ system: params.system,
8869
+ messages: params.turns
8870
+ }
8871
+ ),
8872
+ { signal: params.signal }
8873
+ );
8874
+ for await (const event of stream) {
8875
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
8876
+ state.started = true;
8877
+ yield event.delta.text;
8878
+ }
8879
+ }
8880
+ const final = await stream.finalMessage();
8881
+ if (final.stop_reason === "refusal") {
8882
+ throw new ModelRefusedError(
8883
+ final.stop_details?.explanation || "The model declined to answer that."
8884
+ );
8885
+ }
8886
+ }
8887
+ function describeModelError(err) {
8888
+ const e = err;
8889
+ const detail = (e?.error?.error?.message || "").toLowerCase();
8890
+ const provider = config.ANTHROPIC_BASE_URL ? "The model gateway" : "Anthropic";
8891
+ if (detail.includes("credit balance") || detail.includes("billing")) {
8892
+ return "Hootie's Anthropic account is out of credit. A server admin needs to add credit in the Anthropic console (Plans & Billing).";
8893
+ }
8894
+ switch (e?.status) {
8895
+ case 401:
8896
+ case 403:
8897
+ return `${provider} rejected this server's API key. A server admin can replace it under Settings \u2192 General.`;
8898
+ case 404:
8899
+ return `No model named "${config.HOOTIE_MODEL}" is available on this key. A server admin can change it under Settings \u2192 General.`;
8900
+ case 413:
8901
+ return "That question plus the documents behind it are too large for one request. Try asking something narrower.";
8902
+ case 429:
8903
+ return `${provider} is rate-limiting this server right now. Try again in a moment.`;
8904
+ case 529:
8905
+ return "The model is overloaded right now. Try again in a moment.";
8906
+ }
8907
+ if (typeof e?.status === "number" && e.status >= 500) {
8908
+ return `${provider} is having trouble right now. Try again in a moment.`;
8909
+ }
8910
+ return "Hootie couldn't reach the model. Try again in a moment.";
8911
+ }
8912
+ async function verifyModelKey(apiKey, baseUrl) {
8913
+ try {
8914
+ await clientFor(apiKey, baseUrl).models.list(
8915
+ { limit: 1 },
8916
+ { signal: AbortSignal.timeout(1e4) }
8917
+ );
8918
+ return null;
8919
+ } catch (err) {
8920
+ const status = err?.status;
8921
+ if (status === 401 || status === 403) {
8922
+ return baseUrl ? `The model gateway rejected that key (${status}). Check the key and the gateway URL.` : "Anthropic rejected that key. Check it and paste it again.";
8923
+ }
8924
+ console.warn("[hootie] key check inconclusive, saving anyway", status ?? err?.message);
8925
+ return null;
8926
+ }
8927
+ }
8928
+ var cached = null;
8929
+ function clientFor(apiKey, baseUrl) {
8930
+ if (!cached || cached.apiKey !== apiKey || cached.baseUrl !== baseUrl) {
8931
+ cached = {
8932
+ apiKey,
8933
+ baseUrl,
8934
+ client: new Anthropic({ apiKey, ...baseUrl ? { baseURL: baseUrl } : {} })
8935
+ };
8936
+ }
8937
+ return cached.client;
8938
+ }
8939
+ var ModelRefusedError = class extends Error {
8940
+ constructor(message) {
8941
+ super(message);
8942
+ this.name = "ModelRefusedError";
8943
+ }
8944
+ };
8945
+
8946
+ // src/hootie/routes.ts
8947
+ var hootieRoutes = new Hono15();
8948
+ var MAX_MESSAGE_CHARS = 4e3;
8949
+ var MAX_HISTORY_TURNS = 20;
8950
+ var MAX_HISTORY_CHARS = 8e3;
8951
+ var MAX_HISTORY_RAW = MAX_HISTORY_TURNS * 4;
8952
+ var CONTEXT_BUDGET = 6e3;
8953
+ var CHAT_BUDGET = { max: 30, windowMs: 5 * 6e4 };
8954
+ hootieRoutes.post("/chat", async (c) => {
8955
+ if (!config.HOOTIE_ENABLED) return c.json({ error: "Not found" }, 404);
8956
+ const apiKey = process.env.ANTHROPIC_API_KEY;
8957
+ if (!apiKey) {
8958
+ return c.json(
8959
+ {
8960
+ error: "Hootie needs an Anthropic API key. A server admin can add one under Settings \u2192 General.",
8961
+ reason: "no_api_key"
8962
+ },
8963
+ 503
8964
+ );
8965
+ }
8966
+ let body;
8967
+ try {
8968
+ body = await c.req.json();
8969
+ } catch {
8970
+ return c.json({ error: "Invalid JSON body" }, 400);
8971
+ }
8972
+ const message = typeof body.message === "string" ? body.message.trim() : "";
8973
+ if (!message) return c.json({ error: "message is required" }, 400);
8974
+ if (message.length > MAX_MESSAGE_CHARS) {
8975
+ return c.json(
8976
+ { error: `message is too long (max ${MAX_MESSAGE_CHARS} characters)` },
8977
+ 400
8978
+ );
8979
+ }
8980
+ const nodeId = typeof body.node_id === "string" && body.node_id.trim() ? body.node_id.trim() : null;
8981
+ if (nodeId !== null && !isNodeIdShaped(nodeId)) {
8982
+ return c.json({ error: "Node not found" }, 404);
8983
+ }
8984
+ const history = normalizeHistory(body.history);
8985
+ if (history === null) {
8986
+ return c.json(
8987
+ { error: "history must be a list of {role: user|assistant, content}" },
8988
+ 400
8989
+ );
8990
+ }
8991
+ const nestId = c.req.param("nestId");
8992
+ const userId = c.get("userId");
8993
+ if (!tryConsume(`hootie:${userId}`, CHAT_BUDGET)) {
8994
+ return c.json(
8995
+ { error: "Too many questions in a short time \u2014 give it a few minutes." },
8996
+ 429
8997
+ );
8998
+ }
8999
+ const userEmail = await resolveCallerEmail(userId);
9000
+ const baseUrl = requestBaseUrl(c.req.url);
9001
+ const nodes = [];
9002
+ const blocks = [];
9003
+ const seen = /* @__PURE__ */ new Set();
9004
+ const take = (r) => {
9005
+ for (let i = 0; i < r.nodes.length; i++) {
9006
+ const n = r.nodes[i];
9007
+ if (seen.has(n.id)) continue;
9008
+ seen.add(n.id);
9009
+ nodes.push(n);
9010
+ blocks.push(r.blocks[i].markdown);
9011
+ }
9012
+ };
9013
+ let anchor = null;
9014
+ if (nodeId) {
9015
+ const r = await resolveContext({
9016
+ nestId,
9017
+ userId,
9018
+ baseUrl,
9019
+ body: {
9020
+ selector: `[[${nodeId}]]`,
9021
+ hops: 1,
9022
+ max_tokens: CONTEXT_BUDGET,
9023
+ include_drafts: true
9024
+ }
9025
+ });
9026
+ anchor = r.nodes.find((n) => n.id === nodeId) ?? null;
9027
+ if (!anchor) return c.json({ error: "Node not found" }, 404);
9028
+ take(r);
9029
+ }
9030
+ const question = stripMentions(message);
9031
+ if (question) {
9032
+ const r = await resolveContext({
9033
+ nestId,
9034
+ userId,
9035
+ baseUrl,
9036
+ body: { prompt: question, max_tokens: CONTEXT_BUDGET }
9037
+ });
9038
+ take(r);
9039
+ }
9040
+ const nest = await getNest(nestId);
9041
+ const system = buildSystemPrompt({
9042
+ nestName: nest?.name ?? "this nest",
9043
+ userEmail,
9044
+ anchor,
9045
+ blocks
9046
+ });
9047
+ const turns = [...history, { role: "user", content: message }];
9048
+ const mentionNames = extractMentions(message);
9049
+ trackEvent("hootie.chat", {
9050
+ nestId,
9051
+ anchored: !!anchor,
9052
+ context_nodes: nodes.length,
9053
+ history_turns: history.length,
9054
+ mentions: mentionNames.length
9055
+ });
9056
+ return streamSSE(c, async (stream) => {
9057
+ const cancel = new AbortController();
9058
+ const hangUp = () => cancel.abort();
9059
+ stream.onAbort(hangUp);
9060
+ c.req.raw.signal.addEventListener("abort", hangUp, { once: true });
9061
+ await stream.writeSSE({
9062
+ event: "meta",
9063
+ data: JSON.stringify({ nodes, anchor })
9064
+ });
9065
+ let answer2 = "";
9066
+ try {
9067
+ for await (const chunk of streamAnswer({
9068
+ apiKey,
9069
+ model: config.HOOTIE_MODEL,
9070
+ baseUrl: config.ANTHROPIC_BASE_URL || null,
9071
+ system,
9072
+ turns,
9073
+ signal: cancel.signal
9074
+ })) {
9075
+ if (cancel.signal.aborted) return;
9076
+ answer2 += chunk;
9077
+ await stream.writeSSE({ event: "delta", data: JSON.stringify({ text: chunk }) });
9078
+ }
9079
+ } catch (err) {
9080
+ if (cancel.signal.aborted) return;
9081
+ const text = err instanceof ModelRefusedError ? err.message : describeModelError(err);
9082
+ console.error("[hootie] model call failed", nestId, err);
9083
+ await stream.writeSSE({ event: "error", data: JSON.stringify({ error: text }) });
9084
+ return;
9085
+ }
9086
+ if (cancel.signal.aborted) return;
9087
+ const cited = citedNodes(answer2, nodes);
9088
+ let mention = null;
9089
+ if (mentionNames.length > 0) {
9090
+ mention = await deliverMentions({
9091
+ nestId,
9092
+ userEmail,
9093
+ baseUrl,
9094
+ message,
9095
+ answer: answer2,
9096
+ anchor: anchor ?? cited[0] ?? null
9097
+ });
9098
+ }
9099
+ await stream.writeSSE({
9100
+ event: "done",
9101
+ data: JSON.stringify({ answer: answer2, cited, mention })
9102
+ });
9103
+ });
9104
+ });
9105
+ var NODE_ID_SHAPE = /^nodes(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/;
9106
+ function isNodeIdShaped(id) {
9107
+ return id.length <= 512 && NODE_ID_SHAPE.test(id);
9108
+ }
9109
+ function normalizeHistory(raw) {
9110
+ if (raw === void 0 || raw === null) return [];
9111
+ if (!Array.isArray(raw)) return null;
9112
+ const tail = raw.length > MAX_HISTORY_RAW ? raw.slice(-MAX_HISTORY_RAW) : raw;
9113
+ const turns = [];
9114
+ for (const t of tail) {
9115
+ if (!t || typeof t !== "object") return null;
9116
+ const role = t.role;
9117
+ const content = t.content;
9118
+ if (role !== "user" && role !== "assistant" || typeof content !== "string")
9119
+ return null;
9120
+ const text = content.trim();
9121
+ if (!text) continue;
9122
+ turns.push({ role, content: text });
9123
+ }
9124
+ const merged = [];
9125
+ for (const t of turns) {
9126
+ const last = merged[merged.length - 1];
9127
+ if (last && last.role === t.role) last.content += "\n\n" + t.content;
9128
+ else merged.push({ ...t });
9129
+ }
9130
+ let kept = merged.slice(-MAX_HISTORY_TURNS);
9131
+ let chars = kept.reduce((n, t) => n + t.content.length, 0);
9132
+ while (kept.length > 0 && chars > MAX_HISTORY_CHARS) {
9133
+ chars -= kept[0].content.length;
9134
+ kept = kept.slice(1);
9135
+ }
9136
+ while (kept.length > 0 && kept[0].role !== "user") kept = kept.slice(1);
9137
+ while (kept.length > 0 && kept[kept.length - 1].role !== "assistant")
9138
+ kept = kept.slice(0, -1);
9139
+ return kept;
9140
+ }
9141
+ function buildSystemPrompt(params) {
9142
+ const { nestName: nestName2, userEmail, anchor, blocks } = params;
9143
+ const context = blocks.length > 0 ? blocks.join("\n\n---\n\n").replace(/<\/documents>/gi, "</ documents>") : "(nothing in the nest matched this question)";
9144
+ return [
9145
+ `You are Hootie, the assistant inside the ContextNest nest "${nestName2}". You are talking with ${userEmail}.`,
9146
+ "",
9147
+ "Answer from the documents below and nothing else. They are the governed context this person is allowed to read; do not invent facts that are not in them.",
9148
+ "When you use a document, cite it by its exact title as a wiki link, like [[Title]] \u2014 this is how the reader opens it. Cite every document you drew on, and only those.",
9149
+ "If the documents do not answer the question, say so plainly and, if you can, point to which document comes closest or what to search for. Do not guess.",
9150
+ "Be concise and direct. Use short paragraphs or a short list; no preamble.",
9151
+ "Names written as @someone in the message are colleagues the person is looping in \u2014 they are not questions for you, and you should not address them.",
9152
+ anchor ? `The person currently has the document [[${anchor.title}]] open; "this" in their message means that document unless they say otherwise.` : "The person is looking at the nest as a whole, not a particular document.",
9153
+ "",
9154
+ "<documents>",
9155
+ context,
9156
+ "</documents>"
9157
+ ].join("\n");
9158
+ }
9159
+ function citedNodes(answer2, nodes) {
9160
+ const lower = answer2.toLowerCase();
9161
+ const seenTitles = /* @__PURE__ */ new Set();
9162
+ const hits = [];
9163
+ for (const n of nodes) {
9164
+ const key = n.title.toLowerCase();
9165
+ if (seenTitles.has(key)) continue;
9166
+ const at = lower.indexOf(`[[${key}]]`);
9167
+ if (at < 0) continue;
9168
+ seenTitles.add(key);
9169
+ hits.push({ n, at });
9170
+ }
9171
+ return hits.sort((a, b) => a.at - b.at).map((x) => x.n);
9172
+ }
9173
+ async function deliverMentions(params) {
9174
+ const { nestId, userEmail, baseUrl, message, answer: answer2, anchor } = params;
9175
+ if (!anchor) return { status: "undeliverable", reason: "no_anchor" };
9176
+ const body = [
9177
+ `**Asked Hootie:** ${message}`,
9178
+ "",
9179
+ "**Hootie answered:**",
9180
+ answer2.trim()
9181
+ ].join("\n");
9182
+ let threadId;
9183
+ try {
9184
+ const thread = await createThread(
9185
+ nestId,
9186
+ anchor.id,
9187
+ { body, anchor: null, snapshotVersion: null },
9188
+ userEmail
9189
+ );
9190
+ threadId = thread.id;
9191
+ } catch (err) {
9192
+ console.error("[hootie] mention comment failed", nestId, anchor.id, err);
9193
+ return { status: "undeliverable", reason: "post_failed" };
9194
+ }
9195
+ const notified = await notifyCommentMentions({
9196
+ nestId,
9197
+ nodeId: anchor.id,
9198
+ body: message,
9199
+ actorEmail: userEmail,
9200
+ baseUrl
9201
+ });
9202
+ await syncAnnotationsNode(nestId, anchor.id, userEmail);
9203
+ return {
9204
+ status: "posted",
9205
+ node_id: anchor.id,
9206
+ node_title: anchor.title,
9207
+ thread_id: threadId,
9208
+ notified
9209
+ };
9210
+ }
9211
+
8757
9212
  // src/mcp/routes.ts
8758
- import { Hono as Hono16 } from "hono";
9213
+ import { Hono as Hono17 } from "hono";
8759
9214
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8760
9215
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
8761
9216
 
@@ -9164,7 +9619,7 @@ async function listRuns(nestId, opts) {
9164
9619
  }
9165
9620
 
9166
9621
  // src/workflow/edge-routes.ts
9167
- import { Hono as Hono15 } from "hono";
9622
+ import { Hono as Hono16 } from "hono";
9168
9623
  import { v4 as uuid10 } from "uuid";
9169
9624
  function toResponse(e, t) {
9170
9625
  return {
@@ -9283,7 +9738,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
9283
9738
  );
9284
9739
  }
9285
9740
  }
9286
- var edgeRoutes = new Hono15();
9741
+ var edgeRoutes = new Hono16();
9287
9742
  edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
9288
9743
  const nestId = c.req.param("nestId");
9289
9744
  const db = getDb();
@@ -10434,7 +10889,7 @@ async function resolveLlmBody(ctx, node, gate) {
10434
10889
  const approved = gate ? gate.approvedVersions?.get(node.id) ?? null : await getApprovedVersion(ctx.nestId, node.id);
10435
10890
  if (approved == null) return null;
10436
10891
  try {
10437
- return await ctx.versionManager.reconstructVersion(node.id, approved);
10892
+ return (await reconstructApproved(ctx.versionManager, ctx.nestId, node.id, approved)).raw;
10438
10893
  } catch (err) {
10439
10894
  console.error("reconstructVersion failed", node.id, approved, err);
10440
10895
  return null;
@@ -11470,11 +11925,11 @@ ${list}`,
11470
11925
  if (!target) return "target is required (a node id or folder prefix).";
11471
11926
  if (!["read", "write"].includes(role)) return "role must be read or write.";
11472
11927
  try {
11473
- const { createGrant: createGrant2 } = await import("./grants-service-24GLUBY4.js");
11928
+ const { createGrant: createGrant2 } = await import("./grants-service-WICV4Y2J.js");
11474
11929
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
11475
11930
  const e = normalizeEmail2(String(args.email || ""));
11476
11931
  if (!e) return "email is required.";
11477
- const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-AM6A3WGO.js");
11932
+ const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-XEBX6BM4.js");
11478
11933
  const row = await findOrCreateInvitedUser2(e, { invitedBy: ctx.userEmail });
11479
11934
  await createGrant2({
11480
11935
  nestId: ctx.nestId,
@@ -11528,7 +11983,7 @@ ${list}`;
11528
11983
 
11529
11984
  // src/mcp/routes.ts
11530
11985
  import { z as z2 } from "zod";
11531
- var mcpRoutes = new Hono16();
11986
+ var mcpRoutes = new Hono17();
11532
11987
  async function getUserEmail2(userId) {
11533
11988
  const db = getDb();
11534
11989
  const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
@@ -11598,7 +12053,7 @@ mcpRoutes.all("/", async (c) => {
11598
12053
  });
11599
12054
 
11600
12055
  // src/workflow/run-routes.ts
11601
- import { Hono as Hono17 } from "hono";
12056
+ import { Hono as Hono18 } from "hono";
11602
12057
  import { v4 as uuid12 } from "uuid";
11603
12058
  var RUNNABLE_TYPES2 = ["agent", "skill"];
11604
12059
  var safeJson2 = (s) => {
@@ -11717,7 +12172,7 @@ async function buildRunBundle(nestId, agentNode) {
11717
12172
  definitions
11718
12173
  };
11719
12174
  }
11720
- var runTriggerRoutes = new Hono17();
12175
+ var runTriggerRoutes = new Hono18();
11721
12176
  async function resolveParentRun2(nestId, parentRunId, childAgent) {
11722
12177
  if (!config.FEATURE_SUBAGENT_RUNS) {
11723
12178
  throw new ValidationError(
@@ -11825,7 +12280,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
11825
12280
  201
11826
12281
  );
11827
12282
  });
11828
- var runListRoutes = new Hono17();
12283
+ var runListRoutes = new Hono18();
11829
12284
  runListRoutes.get("/", requireWorkflowPlane, async (c) => {
11830
12285
  const nestId = c.req.param("nestId");
11831
12286
  const db = getDb();
@@ -11857,7 +12312,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
11857
12312
  }))
11858
12313
  });
11859
12314
  });
11860
- var runDetailRoutes = new Hono17();
12315
+ var runDetailRoutes = new Hono18();
11861
12316
  async function loadRunWithPermission(c, runId, required) {
11862
12317
  const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
11863
12318
  runId
@@ -12008,7 +12463,7 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
12008
12463
  });
12009
12464
 
12010
12465
  // src/workflow/schedule-routes.ts
12011
- import { Hono as Hono18 } from "hono";
12466
+ import { Hono as Hono19 } from "hono";
12012
12467
  import { v4 as uuid13 } from "uuid";
12013
12468
  var RUNNABLE_TYPES3 = ["agent", "skill"];
12014
12469
  var MIN_EVERY = 5;
@@ -12046,7 +12501,7 @@ function parseEvery(v) {
12046
12501
  }
12047
12502
  return n;
12048
12503
  }
12049
- var scheduleRoutes = new Hono18();
12504
+ var scheduleRoutes = new Hono19();
12050
12505
  scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
12051
12506
  const nestId = c.req.param("nestId");
12052
12507
  const rows = await getDb().all(
@@ -12142,7 +12597,7 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
12142
12597
  });
12143
12598
 
12144
12599
  // src/notify/connector-routes.ts
12145
- import { Hono as Hono19 } from "hono";
12600
+ import { Hono as Hono20 } from "hono";
12146
12601
  import { v4 as uuid14 } from "uuid";
12147
12602
  var KNOWN_EVENTS = [
12148
12603
  "review_requested",
@@ -12190,7 +12645,7 @@ function parseUrl(v) {
12190
12645
  'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
12191
12646
  );
12192
12647
  }
12193
- var connectorRoutes = new Hono19();
12648
+ var connectorRoutes = new Hono20();
12194
12649
  connectorRoutes.get("/", async (c) => {
12195
12650
  const nestId = c.req.param("nestId");
12196
12651
  const rows = await getDb().all(
@@ -12252,7 +12707,7 @@ connectorRoutes.delete("/:id", async (c) => {
12252
12707
  });
12253
12708
 
12254
12709
  // src/workflow/hook-routes.ts
12255
- import { Hono as Hono20 } from "hono";
12710
+ import { Hono as Hono21 } from "hono";
12256
12711
  import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
12257
12712
  var PRESETS = ["slack", "teams", "webhook"];
12258
12713
  var RUNNABLE_TYPES4 = ["agent", "skill"];
@@ -12275,7 +12730,7 @@ function toResponse4(c, r, opts = {}) {
12275
12730
  created_at: r.created_at
12276
12731
  };
12277
12732
  }
12278
- var hookNestRoutes = new Hono20();
12733
+ var hookNestRoutes = new Hono21();
12279
12734
  hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
12280
12735
  const nestId = c.req.param("nestId");
12281
12736
  const agent = c.req.query("agent");
@@ -12346,10 +12801,10 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
12346
12801
  const mine = createHmac2("sha256", Buffer.from(secret, "base64")).update(rawBody, "utf8").digest("base64");
12347
12802
  return safeEqual(mine, authHeader.slice(5).trim());
12348
12803
  }
12349
- function stripMentions(text) {
12804
+ function stripMentions2(text) {
12350
12805
  return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
12351
12806
  }
12352
- var hookFireRoutes = new Hono20();
12807
+ var hookFireRoutes = new Hono21();
12353
12808
  hookFireRoutes.post("/:token", async (c) => {
12354
12809
  const token = c.req.param("token");
12355
12810
  if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
@@ -12397,7 +12852,7 @@ hookFireRoutes.post("/:token", async (c) => {
12397
12852
  if (payload.type === "event_callback") {
12398
12853
  const ev = payload.event ?? {};
12399
12854
  if (ev.type !== "app_mention") return c.json({ ok: true });
12400
- inputs = { input: stripMentions(String(ev.text ?? "")), from: ev.user, channel: ev.channel };
12855
+ inputs = { input: stripMentions2(String(ev.text ?? "")), from: ev.user, channel: ev.channel };
12401
12856
  ack = () => c.json({ ok: true });
12402
12857
  } else {
12403
12858
  inputs = {
@@ -12415,7 +12870,7 @@ hookFireRoutes.post("/:token", async (c) => {
12415
12870
  if (secret && !teamsSignatureValid(secret, c.req.header("authorization"), raw)) {
12416
12871
  return c.json({ error: "bad signature" }, 401);
12417
12872
  }
12418
- inputs = { input: stripMentions(String(payload.text ?? "")), from: payload.from?.name };
12873
+ inputs = { input: stripMentions2(String(payload.text ?? "")), from: payload.from?.name };
12419
12874
  ack = (runId2) => c.json({ type: "message", text: `\u23F3 Queued ${hook.agent_node} (run ${runId2.slice(0, 12)}\u2026)` });
12420
12875
  } else {
12421
12876
  inputs = Object.keys(payload).length ? payload : null;
@@ -12451,9 +12906,9 @@ hookFireRoutes.post("/:token", async (c) => {
12451
12906
  });
12452
12907
 
12453
12908
  // src/definitions/routes.ts
12454
- import { Hono as Hono21 } from "hono";
12909
+ import { Hono as Hono22 } from "hono";
12455
12910
  import { v4 as uuid15 } from "uuid";
12456
- var definitionRoutes = new Hono21();
12911
+ var definitionRoutes = new Hono22();
12457
12912
  definitionRoutes.get("/", async (c) => {
12458
12913
  const nestId = c.req.param("nestId");
12459
12914
  const db = getDb();
@@ -12561,11 +13016,11 @@ definitionRoutes.delete("/:id", async (c) => {
12561
13016
  });
12562
13017
 
12563
13018
  // src/nests/asset-routes.ts
12564
- import { Hono as Hono22 } from "hono";
13019
+ import { Hono as Hono23 } from "hono";
12565
13020
  import { v4 as uuid16 } from "uuid";
12566
13021
  import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
12567
13022
  import { join as join6 } from "path";
12568
- var assetRoutes = new Hono22();
13023
+ var assetRoutes = new Hono23();
12569
13024
  var CONTENT_TYPES = {
12570
13025
  png: "image/png",
12571
13026
  jpg: "image/jpeg",
@@ -12652,19 +13107,21 @@ assetRoutes.get("/:file", async (c) => {
12652
13107
  });
12653
13108
 
12654
13109
  // src/nodes/pdf-routes.ts
12655
- import { Hono as Hono23 } from "hono";
13110
+ import { Hono as Hono24 } from "hono";
12656
13111
  import { bodyLimit } from "hono/body-limit";
12657
13112
  import { createHash as createHash3 } from "crypto";
12658
13113
  import { isPdf, readPdfBinary as readPdfBinary3 } from "@promptowl/contextnest-engine";
12659
- var pdfNodeRoutes = new Hono23();
13114
+ var pdfNodeRoutes = new Hono24();
12660
13115
  var PDF_UPLOAD_PATH = /^\/nests\/[^/]+\/nodes\/pdf\/?$/;
12661
13116
  function pdfMaxRequestBytes() {
12662
13117
  return config.PDF_MAX_BYTES + 1024 * 1024;
12663
13118
  }
13119
+ function pdfTooLargeMessage(bytes) {
13120
+ const size = bytes === void 0 ? "" : `${(bytes / (1024 * 1024)).toFixed(1)} MB, `;
13121
+ return `PDF too large (${size}max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`;
13122
+ }
12664
13123
  function tooLarge(bytes) {
12665
- return new PayloadTooLargeError(
12666
- `PDF too large (${(bytes / (1024 * 1024)).toFixed(1)} MB, max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
12667
- );
13124
+ return new PayloadTooLargeError(pdfTooLargeMessage(bytes));
12668
13125
  }
12669
13126
  function field(body, name) {
12670
13127
  const v = body[name];
@@ -12685,12 +13142,7 @@ pdfNodeRoutes.post(
12685
13142
  "/pdf",
12686
13143
  (c, next) => bodyLimit({
12687
13144
  maxSize: pdfMaxRequestBytes(),
12688
- onError: (c2) => c2.json(
12689
- {
12690
- error: `PDF too large (max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
12691
- },
12692
- 413
12693
- )
13145
+ onError: (c2) => c2.json({ error: pdfTooLargeMessage() }, 413)
12694
13146
  })(c, next),
12695
13147
  async (c) => {
12696
13148
  const nestId = c.req.param("nestId");
@@ -12829,7 +13281,7 @@ pdfNodeRoutes.get("/:nodeId{.+}/pdf", async (c, next) => {
12829
13281
  });
12830
13282
 
12831
13283
  // src/auth/community-sites-routes.ts
12832
- import { Hono as Hono24 } from "hono";
13284
+ import { Hono as Hono25 } from "hono";
12833
13285
  var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
12834
13286
  var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
12835
13287
  var serializeSite = (site) => ({
@@ -12841,7 +13293,7 @@ var serializeSite = (site) => ({
12841
13293
  created_at: site.created_at,
12842
13294
  updated_at: site.updated_at
12843
13295
  });
12844
- var communitySiteRoutes = new Hono24();
13296
+ var communitySiteRoutes = new Hono25();
12845
13297
  communitySiteRoutes.get("/", async (c) => {
12846
13298
  if (!await serverAdminAllowed(c))
12847
13299
  return c.json({ error: "Only the server admin can view this." }, 403);
@@ -12929,7 +13381,7 @@ communitySiteRoutes.delete("/:id", async (c) => {
12929
13381
  });
12930
13382
 
12931
13383
  // src/mcp/server-routes.ts
12932
- import { Hono as Hono25 } from "hono";
13384
+ import { Hono as Hono26 } from "hono";
12933
13385
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
12934
13386
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
12935
13387
  import { z as z3 } from "zod";
@@ -13265,7 +13717,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
13265
13717
  }
13266
13718
  return server;
13267
13719
  }
13268
- var serverMcpRoutes = new Hono25();
13720
+ var serverMcpRoutes = new Hono26();
13269
13721
  serverMcpRoutes.all("/", async (c) => {
13270
13722
  const userId = c.get("userId");
13271
13723
  const userEmail = await getUserEmail3(userId);
@@ -13288,7 +13740,7 @@ serverMcpRoutes.all("/", async (c) => {
13288
13740
  await server.server.close();
13289
13741
  }
13290
13742
  });
13291
- var nestIndexRoutes = new Hono25();
13743
+ var nestIndexRoutes = new Hono26();
13292
13744
  nestIndexRoutes.get("/", async (c) => {
13293
13745
  const userId = c.get("userId");
13294
13746
  const nests = await buildNestIndex(userId, c.get("nestScope"));
@@ -13464,14 +13916,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
13464
13916
  through steward review before they become AI-visible.
13465
13917
  `;
13466
13918
  }
13467
- var llmsTxtRoutes = new Hono25();
13919
+ var llmsTxtRoutes = new Hono26();
13468
13920
  llmsTxtRoutes.get("/", (c) => {
13469
13921
  const md = renderLlmsTxt(requestBaseUrl(c.req.url));
13470
13922
  return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
13471
13923
  });
13472
13924
 
13473
13925
  // src/governance/routes.ts
13474
- import { Hono as Hono26 } from "hono";
13926
+ import { Hono as Hono27 } from "hono";
13475
13927
 
13476
13928
  // src/governance/feed-service.ts
13477
13929
  var SNIPPET_CHARS = 200;
@@ -13903,7 +14355,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
13903
14355
  if (Number.isNaN(n) || n < 1) return def;
13904
14356
  return Math.min(n, max);
13905
14357
  }
13906
- var governanceRoutes = new Hono26();
14358
+ var governanceRoutes = new Hono27();
13907
14359
  governanceRoutes.get("/stewards", async (c) => {
13908
14360
  const nestId = c.req.param("nestId");
13909
14361
  const scope = c.req.query("scope");
@@ -14273,7 +14725,7 @@ governanceRoutes.get("/activity", async (c) => {
14273
14725
  const activity = await getActivity({ nestId, limit });
14274
14726
  return c.json({ activity });
14275
14727
  });
14276
- var governanceNodeRoutes = new Hono26();
14728
+ var governanceNodeRoutes = new Hono27();
14277
14729
  governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
14278
14730
  const nestId = c.req.param("nestId");
14279
14731
  const nodeId = c.req.param("nodeId");
@@ -14905,7 +15357,7 @@ async function canApproveInNest(nestId, email) {
14905
15357
  return ok;
14906
15358
  }
14907
15359
  function createApp() {
14908
- const app = new Hono27({ router: new LinearRouter() });
15360
+ const app = new Hono28({ router: new LinearRouter() });
14909
15361
  const corsOrigins = config.CORS_ORIGINS;
14910
15362
  app.use(
14911
15363
  "*",
@@ -14924,11 +15376,13 @@ function createApp() {
14924
15376
  if (lenStr) {
14925
15377
  const len = parseInt(lenStr, 10);
14926
15378
  const isPost = c.req.method === "POST";
14927
- const cap = isPost && ASSET_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, ASSET_MAX_REQUEST_BYTES) : isPost && PDF_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, pdfMaxRequestBytes()) : config.MAX_BODY_BYTES;
15379
+ const isPdf2 = isPost && PDF_UPLOAD_PATH.test(c.req.path);
15380
+ const cap = isPost && ASSET_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, ASSET_MAX_REQUEST_BYTES) : isPdf2 ? Math.max(config.MAX_BODY_BYTES, pdfMaxRequestBytes()) : config.MAX_BODY_BYTES;
14928
15381
  if (Number.isFinite(len) && len > cap) {
14929
15382
  return c.json(
14930
15383
  {
14931
- error: `Request body too large (${len} bytes, max ${cap})`
15384
+ // A PDF gets the same message as the route's own gates.
15385
+ error: isPdf2 ? pdfTooLargeMessage(len) : `Request body too large (${len} bytes, max ${cap})`
14932
15386
  },
14933
15387
  413
14934
15388
  );
@@ -15001,6 +15455,11 @@ function createApp() {
15001
15455
  // API would refuse it — existing nodes of the type stay readable.
15002
15456
  type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
15003
15457
  type_table_enabled: config.TYPE_TABLE_ENABLED,
15458
+ // Hootie in the nest (default ON). Off → POST /nests/:id/chat 404s and
15459
+ // the UI hides the launcher. Whether it can actually answer is
15460
+ // runner_key_configured above — the UI shows the launcher whenever the
15461
+ // feature is on and explains the missing key, rather than hiding it.
15462
+ hootie_enabled: config.HOOTIE_ENABLED,
15004
15463
  // This server hosts several organizations (PromptOwl-hosted) rather than
15005
15464
  // one. The visibility picker words 'Organization' and 'Public' by it and
15006
15465
  // confirms before 'org'; the server enforces the same in
@@ -15115,6 +15574,9 @@ function createApp() {
15115
15574
  promptowl_teams_enabled: config.PROMPTOWL_TEAMS_ENABLED,
15116
15575
  type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
15117
15576
  type_table_enabled: config.TYPE_TABLE_ENABLED,
15577
+ hootie_enabled: config.HOOTIE_ENABLED,
15578
+ hootie_model: config.HOOTIE_MODEL,
15579
+ anthropic_base_url: config.ANTHROPIC_BASE_URL,
15118
15580
  subagent_runs_enabled: config.FEATURE_SUBAGENT_RUNS,
15119
15581
  subagent_max_depth: config.SUBAGENT_MAX_DEPTH,
15120
15582
  subagent_max_children: config.SUBAGENT_MAX_CHILDREN,
@@ -15253,6 +15715,38 @@ function createApp() {
15253
15715
  value: body.type_table_enabled ? null : "false"
15254
15716
  });
15255
15717
  }
15718
+ if ("hootie_enabled" in body) {
15719
+ pending.push({
15720
+ name: "HOOTIE_ENABLED",
15721
+ value: body.hootie_enabled ? null : "false"
15722
+ });
15723
+ }
15724
+ if ("hootie_model" in body) {
15725
+ const v = String(body.hootie_model ?? "").trim();
15726
+ if (!v) addError("hootie_model", "Model is required \u2014 e.g. claude-opus-5.");
15727
+ else if (v.length > 200 || /\s/.test(v))
15728
+ addError("hootie_model", "Model must be a single identifier with no spaces.");
15729
+ else pending.push({ name: "HOOTIE_MODEL", value: v === "claude-opus-5" ? null : v });
15730
+ }
15731
+ if ("anthropic_base_url" in body) {
15732
+ const v = String(body.anthropic_base_url ?? "").trim().replace(/\/$/, "");
15733
+ if (!v) pending.push({ name: "ANTHROPIC_BASE_URL", value: null });
15734
+ else {
15735
+ let ok = false;
15736
+ try {
15737
+ const u = new URL(v);
15738
+ ok = (u.protocol === "http:" || u.protocol === "https:") && !!u.host;
15739
+ } catch {
15740
+ ok = false;
15741
+ }
15742
+ if (!ok)
15743
+ addError(
15744
+ "anthropic_base_url",
15745
+ "Model gateway URL must be an http(s) URL, e.g. http://gateway:4000 \u2014 or empty to call Anthropic directly."
15746
+ );
15747
+ else pending.push({ name: "ANTHROPIC_BASE_URL", value: v });
15748
+ }
15749
+ }
15256
15750
  if ("anthropic_api_key" in body) {
15257
15751
  const v = body.anthropic_api_key;
15258
15752
  if (v === null || v === "") {
@@ -15260,7 +15754,12 @@ function createApp() {
15260
15754
  } else if (typeof v !== "string" || !v.trim().startsWith("sk-")) {
15261
15755
  addError("anthropic_api_key", 'Anthropic API key must start with "sk-" (or be empty to clear it).');
15262
15756
  } else {
15263
- pending.push({ name: "ANTHROPIC_API_KEY", value: v.trim() });
15757
+ const key = v.trim();
15758
+ const patchedBase = pending.find((p) => p.name === "ANTHROPIC_BASE_URL");
15759
+ const baseUrl = patchedBase ? patchedBase.value : config.ANTHROPIC_BASE_URL || null;
15760
+ const rejected = await verifyModelKey(key, baseUrl);
15761
+ if (rejected) addError("anthropic_api_key", rejected);
15762
+ else pending.push({ name: "ANTHROPIC_API_KEY", value: key });
15264
15763
  }
15265
15764
  }
15266
15765
  if ("subagent_runs_enabled" in body) {
@@ -15976,7 +16475,7 @@ function createApp() {
15976
16475
  app.route("/index", nestIndexRoutes);
15977
16476
  app.use("/mcp", flexAuthWithPromptowlTicket);
15978
16477
  app.route("/mcp", serverMcpRoutes);
15979
- const nestsApp = new Hono27();
16478
+ const nestsApp = new Hono28();
15980
16479
  nestsApp.use("*", async (c, next) => {
15981
16480
  const parts = nestPathParts(c.req.path);
15982
16481
  const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
@@ -16055,7 +16554,7 @@ function createApp() {
16055
16554
  if (access.archived && isMcpTransport) {
16056
16555
  return deny(404, "Nest not found");
16057
16556
  }
16058
- const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
16557
+ const isReadQuery = parts.length === 2 && ["context", "query", "search", "export", "chat"].includes(parts[1]);
16059
16558
  let grantRole = null;
16060
16559
  let grantNestVisible = false;
16061
16560
  if (permission === "none") {
@@ -16202,6 +16701,7 @@ function createApp() {
16202
16701
  nestsApp.route("/:nestId/nodes", governanceNodeRoutes);
16203
16702
  nestsApp.route("/:nestId/nodes", nodeRoutes);
16204
16703
  nestsApp.route("/:nestId", queryRoutes);
16704
+ nestsApp.route("/:nestId", hootieRoutes);
16205
16705
  nestsApp.route("/:nestId", sharingRoutes);
16206
16706
  nestsApp.route("/:nestId", brandingRoutes);
16207
16707
  nestsApp.route("/:nestId/grants", grantRoutes);