brains-mcp 1.61.2

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 (350) hide show
  1. package/README.md +95 -0
  2. package/dist/access/projectShares.d.ts +48 -0
  3. package/dist/access/projectShares.d.ts.map +1 -0
  4. package/dist/access/projectShares.js +203 -0
  5. package/dist/access/projectShares.js.map +1 -0
  6. package/dist/access/sharedInstructionsNote.d.ts +37 -0
  7. package/dist/access/sharedInstructionsNote.d.ts.map +1 -0
  8. package/dist/access/sharedInstructionsNote.js +76 -0
  9. package/dist/access/sharedInstructionsNote.js.map +1 -0
  10. package/dist/admin/seedUsers.d.ts +23 -0
  11. package/dist/admin/seedUsers.d.ts.map +1 -0
  12. package/dist/admin/seedUsers.js +168 -0
  13. package/dist/admin/seedUsers.js.map +1 -0
  14. package/dist/api/openapi.d.ts +2 -0
  15. package/dist/api/openapi.d.ts.map +1 -0
  16. package/dist/api/openapi.js +942 -0
  17. package/dist/api/openapi.js.map +1 -0
  18. package/dist/auth/clientStore.d.ts +43 -0
  19. package/dist/auth/clientStore.d.ts.map +1 -0
  20. package/dist/auth/clientStore.js +168 -0
  21. package/dist/auth/clientStore.js.map +1 -0
  22. package/dist/auth/deleteAccount.d.ts +34 -0
  23. package/dist/auth/deleteAccount.d.ts.map +1 -0
  24. package/dist/auth/deleteAccount.js +203 -0
  25. package/dist/auth/deleteAccount.js.map +1 -0
  26. package/dist/auth/oauth.d.ts +118 -0
  27. package/dist/auth/oauth.d.ts.map +1 -0
  28. package/dist/auth/oauth.js +491 -0
  29. package/dist/auth/oauth.js.map +1 -0
  30. package/dist/auth/patStore.d.ts +87 -0
  31. package/dist/auth/patStore.d.ts.map +1 -0
  32. package/dist/auth/patStore.js +308 -0
  33. package/dist/auth/patStore.js.map +1 -0
  34. package/dist/auth/supabase.d.ts +15 -0
  35. package/dist/auth/supabase.d.ts.map +1 -0
  36. package/dist/auth/supabase.js +66 -0
  37. package/dist/auth/supabase.js.map +1 -0
  38. package/dist/auth/tokenStore.d.ts +73 -0
  39. package/dist/auth/tokenStore.d.ts.map +1 -0
  40. package/dist/auth/tokenStore.js +261 -0
  41. package/dist/auth/tokenStore.js.map +1 -0
  42. package/dist/backup/drive.d.ts +24 -0
  43. package/dist/backup/drive.d.ts.map +1 -0
  44. package/dist/backup/drive.js +70 -0
  45. package/dist/backup/drive.js.map +1 -0
  46. package/dist/billing/tierStore.d.ts +59 -0
  47. package/dist/billing/tierStore.d.ts.map +1 -0
  48. package/dist/billing/tierStore.js +152 -0
  49. package/dist/billing/tierStore.js.map +1 -0
  50. package/dist/digest/activityDigest.d.ts +32 -0
  51. package/dist/digest/activityDigest.d.ts.map +1 -0
  52. package/dist/digest/activityDigest.js +299 -0
  53. package/dist/digest/activityDigest.js.map +1 -0
  54. package/dist/digest/digestCron.d.ts +40 -0
  55. package/dist/digest/digestCron.d.ts.map +1 -0
  56. package/dist/digest/digestCron.js +146 -0
  57. package/dist/digest/digestCron.js.map +1 -0
  58. package/dist/digest/digestPreferences.d.ts +25 -0
  59. package/dist/digest/digestPreferences.d.ts.map +1 -0
  60. package/dist/digest/digestPreferences.js +79 -0
  61. package/dist/digest/digestPreferences.js.map +1 -0
  62. package/dist/digest/emailSender.d.ts +20 -0
  63. package/dist/digest/emailSender.d.ts.map +1 -0
  64. package/dist/digest/emailSender.js +35 -0
  65. package/dist/digest/emailSender.js.map +1 -0
  66. package/dist/drive/client.d.ts +43 -0
  67. package/dist/drive/client.d.ts.map +1 -0
  68. package/dist/drive/client.js +282 -0
  69. package/dist/drive/client.js.map +1 -0
  70. package/dist/drive/errors.d.ts +27 -0
  71. package/dist/drive/errors.d.ts.map +1 -0
  72. package/dist/drive/errors.js +51 -0
  73. package/dist/drive/errors.js.map +1 -0
  74. package/dist/drive/fileCache.d.ts +22 -0
  75. package/dist/drive/fileCache.d.ts.map +1 -0
  76. package/dist/drive/fileCache.js +57 -0
  77. package/dist/drive/fileCache.js.map +1 -0
  78. package/dist/governance/accessControl.d.ts +87 -0
  79. package/dist/governance/accessControl.d.ts.map +1 -0
  80. package/dist/governance/accessControl.js +179 -0
  81. package/dist/governance/accessControl.js.map +1 -0
  82. package/dist/html/components.d.ts +8 -0
  83. package/dist/html/components.d.ts.map +1 -0
  84. package/dist/html/components.js +19 -0
  85. package/dist/html/components.js.map +1 -0
  86. package/dist/html/layout.d.ts +7 -0
  87. package/dist/html/layout.d.ts.map +1 -0
  88. package/dist/html/layout.js +24 -0
  89. package/dist/html/layout.js.map +1 -0
  90. package/dist/html/metricsUi.d.ts +8 -0
  91. package/dist/html/metricsUi.d.ts.map +1 -0
  92. package/dist/html/metricsUi.js +271 -0
  93. package/dist/html/metricsUi.js.map +1 -0
  94. package/dist/httpServer.d.ts +19 -0
  95. package/dist/httpServer.d.ts.map +1 -0
  96. package/dist/httpServer.js +3913 -0
  97. package/dist/httpServer.js.map +1 -0
  98. package/dist/index.d.ts +3 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +40 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/jobs/asyncJobRegistry.d.ts +30 -0
  103. package/dist/jobs/asyncJobRegistry.d.ts.map +1 -0
  104. package/dist/jobs/asyncJobRegistry.js +70 -0
  105. package/dist/jobs/asyncJobRegistry.js.map +1 -0
  106. package/dist/observability/metricsQuery.d.ts +41 -0
  107. package/dist/observability/metricsQuery.d.ts.map +1 -0
  108. package/dist/observability/metricsQuery.js +106 -0
  109. package/dist/observability/metricsQuery.js.map +1 -0
  110. package/dist/observability/relevanceSignal.d.ts +26 -0
  111. package/dist/observability/relevanceSignal.d.ts.map +1 -0
  112. package/dist/observability/relevanceSignal.js +95 -0
  113. package/dist/observability/relevanceSignal.js.map +1 -0
  114. package/dist/observability/requestEvents.d.ts +31 -0
  115. package/dist/observability/requestEvents.d.ts.map +1 -0
  116. package/dist/observability/requestEvents.js +54 -0
  117. package/dist/observability/requestEvents.js.map +1 -0
  118. package/dist/observability/slowQueryAlert.d.ts +25 -0
  119. package/dist/observability/slowQueryAlert.d.ts.map +1 -0
  120. package/dist/observability/slowQueryAlert.js +67 -0
  121. package/dist/observability/slowQueryAlert.js.map +1 -0
  122. package/dist/observability/zeroResultTriage.d.ts +22 -0
  123. package/dist/observability/zeroResultTriage.d.ts.map +1 -0
  124. package/dist/observability/zeroResultTriage.js +116 -0
  125. package/dist/observability/zeroResultTriage.js.map +1 -0
  126. package/dist/search/embeddings.d.ts +31 -0
  127. package/dist/search/embeddings.d.ts.map +1 -0
  128. package/dist/search/embeddings.js +91 -0
  129. package/dist/search/embeddings.js.map +1 -0
  130. package/dist/search/fileWatcher.d.ts +28 -0
  131. package/dist/search/fileWatcher.d.ts.map +1 -0
  132. package/dist/search/fileWatcher.js +103 -0
  133. package/dist/search/fileWatcher.js.map +1 -0
  134. package/dist/search/hotQueryCache.d.ts +31 -0
  135. package/dist/search/hotQueryCache.d.ts.map +1 -0
  136. package/dist/search/hotQueryCache.js +103 -0
  137. package/dist/search/hotQueryCache.js.map +1 -0
  138. package/dist/search/postgresIndex.d.ts +439 -0
  139. package/dist/search/postgresIndex.d.ts.map +1 -0
  140. package/dist/search/postgresIndex.js +1610 -0
  141. package/dist/search/postgresIndex.js.map +1 -0
  142. package/dist/search/sqliteIndex.d.ts +66 -0
  143. package/dist/search/sqliteIndex.d.ts.map +1 -0
  144. package/dist/search/sqliteIndex.js +342 -0
  145. package/dist/search/sqliteIndex.js.map +1 -0
  146. package/dist/server.d.ts +43 -0
  147. package/dist/server.d.ts.map +1 -0
  148. package/dist/server.js +532 -0
  149. package/dist/server.js.map +1 -0
  150. package/dist/session/bundle.d.ts +53 -0
  151. package/dist/session/bundle.d.ts.map +1 -0
  152. package/dist/session/bundle.js +97 -0
  153. package/dist/session/bundle.js.map +1 -0
  154. package/dist/storage/client.d.ts +157 -0
  155. package/dist/storage/client.d.ts.map +1 -0
  156. package/dist/storage/client.js +825 -0
  157. package/dist/storage/client.js.map +1 -0
  158. package/dist/tools/addWebhookPattern.d.ts +15 -0
  159. package/dist/tools/addWebhookPattern.d.ts.map +1 -0
  160. package/dist/tools/addWebhookPattern.js +15 -0
  161. package/dist/tools/addWebhookPattern.js.map +1 -0
  162. package/dist/tools/appendLog.d.ts +18 -0
  163. package/dist/tools/appendLog.d.ts.map +1 -0
  164. package/dist/tools/appendLog.js +18 -0
  165. package/dist/tools/appendLog.js.map +1 -0
  166. package/dist/tools/auditIndex.d.ts +15 -0
  167. package/dist/tools/auditIndex.d.ts.map +1 -0
  168. package/dist/tools/auditIndex.js +165 -0
  169. package/dist/tools/auditIndex.js.map +1 -0
  170. package/dist/tools/batchDeletePages.d.ts +18 -0
  171. package/dist/tools/batchDeletePages.d.ts.map +1 -0
  172. package/dist/tools/batchDeletePages.js +143 -0
  173. package/dist/tools/batchDeletePages.js.map +1 -0
  174. package/dist/tools/coerce.d.ts +3 -0
  175. package/dist/tools/coerce.d.ts.map +1 -0
  176. package/dist/tools/coerce.js +12 -0
  177. package/dist/tools/coerce.js.map +1 -0
  178. package/dist/tools/createPage.d.ts +24 -0
  179. package/dist/tools/createPage.d.ts.map +1 -0
  180. package/dist/tools/createPage.js +99 -0
  181. package/dist/tools/createPage.js.map +1 -0
  182. package/dist/tools/createPages.d.ts +51 -0
  183. package/dist/tools/createPages.d.ts.map +1 -0
  184. package/dist/tools/createPages.js +91 -0
  185. package/dist/tools/createPages.js.map +1 -0
  186. package/dist/tools/deletePage.d.ts +19 -0
  187. package/dist/tools/deletePage.d.ts.map +1 -0
  188. package/dist/tools/deletePage.js +127 -0
  189. package/dist/tools/deletePage.js.map +1 -0
  190. package/dist/tools/exportWiki.d.ts +48 -0
  191. package/dist/tools/exportWiki.d.ts.map +1 -0
  192. package/dist/tools/exportWiki.js +233 -0
  193. package/dist/tools/exportWiki.js.map +1 -0
  194. package/dist/tools/extractLinks.d.ts +14 -0
  195. package/dist/tools/extractLinks.d.ts.map +1 -0
  196. package/dist/tools/extractLinks.js +25 -0
  197. package/dist/tools/extractLinks.js.map +1 -0
  198. package/dist/tools/generateClaudeInstructions.d.ts +27 -0
  199. package/dist/tools/generateClaudeInstructions.d.ts.map +1 -0
  200. package/dist/tools/generateClaudeInstructions.js +263 -0
  201. package/dist/tools/generateClaudeInstructions.js.map +1 -0
  202. package/dist/tools/generateGptInstructions.d.ts +27 -0
  203. package/dist/tools/generateGptInstructions.d.ts.map +1 -0
  204. package/dist/tools/generateGptInstructions.js +208 -0
  205. package/dist/tools/generateGptInstructions.js.map +1 -0
  206. package/dist/tools/getBacklinks.d.ts +12 -0
  207. package/dist/tools/getBacklinks.d.ts.map +1 -0
  208. package/dist/tools/getBacklinks.js +13 -0
  209. package/dist/tools/getBacklinks.js.map +1 -0
  210. package/dist/tools/getRelated.d.ts +12 -0
  211. package/dist/tools/getRelated.d.ts.map +1 -0
  212. package/dist/tools/getRelated.js +13 -0
  213. package/dist/tools/getRelated.js.map +1 -0
  214. package/dist/tools/healthCheck.d.ts +5 -0
  215. package/dist/tools/healthCheck.d.ts.map +1 -0
  216. package/dist/tools/healthCheck.js +167 -0
  217. package/dist/tools/healthCheck.js.map +1 -0
  218. package/dist/tools/importBrainSource.d.ts +36 -0
  219. package/dist/tools/importBrainSource.d.ts.map +1 -0
  220. package/dist/tools/importBrainSource.js +314 -0
  221. package/dist/tools/importBrainSource.js.map +1 -0
  222. package/dist/tools/importWikiZip.d.ts +60 -0
  223. package/dist/tools/importWikiZip.d.ts.map +1 -0
  224. package/dist/tools/importWikiZip.js +379 -0
  225. package/dist/tools/importWikiZip.js.map +1 -0
  226. package/dist/tools/initProject.d.ts +34 -0
  227. package/dist/tools/initProject.d.ts.map +1 -0
  228. package/dist/tools/initProject.js +443 -0
  229. package/dist/tools/initProject.js.map +1 -0
  230. package/dist/tools/instructionContext.d.ts +60 -0
  231. package/dist/tools/instructionContext.d.ts.map +1 -0
  232. package/dist/tools/instructionContext.js +210 -0
  233. package/dist/tools/instructionContext.js.map +1 -0
  234. package/dist/tools/listArchivedPages.d.ts +17 -0
  235. package/dist/tools/listArchivedPages.d.ts.map +1 -0
  236. package/dist/tools/listArchivedPages.js +51 -0
  237. package/dist/tools/listArchivedPages.js.map +1 -0
  238. package/dist/tools/listPages.d.ts +28 -0
  239. package/dist/tools/listPages.d.ts.map +1 -0
  240. package/dist/tools/listPages.js +166 -0
  241. package/dist/tools/listPages.js.map +1 -0
  242. package/dist/tools/listWebhooks.d.ts +7 -0
  243. package/dist/tools/listWebhooks.d.ts.map +1 -0
  244. package/dist/tools/listWebhooks.js +10 -0
  245. package/dist/tools/listWebhooks.js.map +1 -0
  246. package/dist/tools/movePage.d.ts +18 -0
  247. package/dist/tools/movePage.d.ts.map +1 -0
  248. package/dist/tools/movePage.js +182 -0
  249. package/dist/tools/movePage.js.map +1 -0
  250. package/dist/tools/pageHistory.d.ts +36 -0
  251. package/dist/tools/pageHistory.d.ts.map +1 -0
  252. package/dist/tools/pageHistory.js +60 -0
  253. package/dist/tools/pageHistory.js.map +1 -0
  254. package/dist/tools/patchPage.d.ts +141 -0
  255. package/dist/tools/patchPage.d.ts.map +1 -0
  256. package/dist/tools/patchPage.js +287 -0
  257. package/dist/tools/patchPage.js.map +1 -0
  258. package/dist/tools/pullSessionBundle.d.ts +34 -0
  259. package/dist/tools/pullSessionBundle.d.ts.map +1 -0
  260. package/dist/tools/pullSessionBundle.js +432 -0
  261. package/dist/tools/pullSessionBundle.js.map +1 -0
  262. package/dist/tools/purgeWiki.d.ts +23 -0
  263. package/dist/tools/purgeWiki.d.ts.map +1 -0
  264. package/dist/tools/purgeWiki.js +113 -0
  265. package/dist/tools/purgeWiki.js.map +1 -0
  266. package/dist/tools/pushSessionBundle.d.ts +139 -0
  267. package/dist/tools/pushSessionBundle.d.ts.map +1 -0
  268. package/dist/tools/pushSessionBundle.js +282 -0
  269. package/dist/tools/pushSessionBundle.js.map +1 -0
  270. package/dist/tools/readGovernanceAudit.d.ts +17 -0
  271. package/dist/tools/readGovernanceAudit.d.ts.map +1 -0
  272. package/dist/tools/readGovernanceAudit.js +21 -0
  273. package/dist/tools/readGovernanceAudit.js.map +1 -0
  274. package/dist/tools/readPage.d.ts +13 -0
  275. package/dist/tools/readPage.d.ts.map +1 -0
  276. package/dist/tools/readPage.js +46 -0
  277. package/dist/tools/readPage.js.map +1 -0
  278. package/dist/tools/removeWebhook.d.ts +14 -0
  279. package/dist/tools/removeWebhook.d.ts.map +1 -0
  280. package/dist/tools/removeWebhook.js +12 -0
  281. package/dist/tools/removeWebhook.js.map +1 -0
  282. package/dist/tools/resolveProject.d.ts +58 -0
  283. package/dist/tools/resolveProject.d.ts.map +1 -0
  284. package/dist/tools/resolveProject.js +175 -0
  285. package/dist/tools/resolveProject.js.map +1 -0
  286. package/dist/tools/restorePage.d.ts +47 -0
  287. package/dist/tools/restorePage.d.ts.map +1 -0
  288. package/dist/tools/restorePage.js +111 -0
  289. package/dist/tools/restorePage.js.map +1 -0
  290. package/dist/tools/searchPages.d.ts +28 -0
  291. package/dist/tools/searchPages.d.ts.map +1 -0
  292. package/dist/tools/searchPages.js +372 -0
  293. package/dist/tools/searchPages.js.map +1 -0
  294. package/dist/tools/setWebhook.d.ts +30 -0
  295. package/dist/tools/setWebhook.d.ts.map +1 -0
  296. package/dist/tools/setWebhook.js +30 -0
  297. package/dist/tools/setWebhook.js.map +1 -0
  298. package/dist/tools/shareProject.d.ts +25 -0
  299. package/dist/tools/shareProject.d.ts.map +1 -0
  300. package/dist/tools/shareProject.js +56 -0
  301. package/dist/tools/shareProject.js.map +1 -0
  302. package/dist/tools/suggestLinks.d.ts +29 -0
  303. package/dist/tools/suggestLinks.d.ts.map +1 -0
  304. package/dist/tools/suggestLinks.js +214 -0
  305. package/dist/tools/suggestLinks.js.map +1 -0
  306. package/dist/tools/updatePage.d.ts +27 -0
  307. package/dist/tools/updatePage.d.ts.map +1 -0
  308. package/dist/tools/updatePage.js +118 -0
  309. package/dist/tools/updatePage.js.map +1 -0
  310. package/dist/webhooks/dispatch.d.ts +49 -0
  311. package/dist/webhooks/dispatch.d.ts.map +1 -0
  312. package/dist/webhooks/dispatch.js +159 -0
  313. package/dist/webhooks/dispatch.js.map +1 -0
  314. package/dist/webhooks/webhookRules.d.ts +34 -0
  315. package/dist/webhooks/webhookRules.d.ts.map +1 -0
  316. package/dist/webhooks/webhookRules.js +129 -0
  317. package/dist/webhooks/webhookRules.js.map +1 -0
  318. package/dist/wiki/frontmatter.d.ts +45 -0
  319. package/dist/wiki/frontmatter.d.ts.map +1 -0
  320. package/dist/wiki/frontmatter.js +68 -0
  321. package/dist/wiki/frontmatter.js.map +1 -0
  322. package/dist/wiki/index.d.ts +22 -0
  323. package/dist/wiki/index.d.ts.map +1 -0
  324. package/dist/wiki/index.js +203 -0
  325. package/dist/wiki/index.js.map +1 -0
  326. package/dist/wiki/ingestPolicy.d.ts +18 -0
  327. package/dist/wiki/ingestPolicy.d.ts.map +1 -0
  328. package/dist/wiki/ingestPolicy.js +64 -0
  329. package/dist/wiki/ingestPolicy.js.map +1 -0
  330. package/dist/wiki/layoutLint.d.ts +51 -0
  331. package/dist/wiki/layoutLint.d.ts.map +1 -0
  332. package/dist/wiki/layoutLint.js +146 -0
  333. package/dist/wiki/layoutLint.js.map +1 -0
  334. package/dist/wiki/links.d.ts +6 -0
  335. package/dist/wiki/links.d.ts.map +1 -0
  336. package/dist/wiki/links.js +45 -0
  337. package/dist/wiki/links.js.map +1 -0
  338. package/dist/wiki/log.d.ts +58 -0
  339. package/dist/wiki/log.d.ts.map +1 -0
  340. package/dist/wiki/log.js +126 -0
  341. package/dist/wiki/log.js.map +1 -0
  342. package/dist/wiki/path.d.ts +8 -0
  343. package/dist/wiki/path.d.ts.map +1 -0
  344. package/dist/wiki/path.js +25 -0
  345. package/dist/wiki/path.js.map +1 -0
  346. package/dist/wiki/storageLimits.d.ts +163 -0
  347. package/dist/wiki/storageLimits.d.ts.map +1 -0
  348. package/dist/wiki/storageLimits.js +281 -0
  349. package/dist/wiki/storageLimits.js.map +1 -0
  350. package/package.json +81 -0
@@ -0,0 +1,214 @@
1
+ import { z } from "zod";
2
+ import { parsePage } from "../wiki/frontmatter.js";
3
+ import { getPostgresSearchIndex } from "../search/postgresIndex.js";
4
+ import { extractWikiLinks } from "../wiki/links.js";
5
+ export const suggestLinksInputSchema = z.object({
6
+ tags: z
7
+ .array(z.string())
8
+ .optional()
9
+ .describe("Filter pages by tag (any match). At least one of tags or folder is required."),
10
+ folder: z
11
+ .string()
12
+ .optional()
13
+ .describe("Filter pages by name prefix (e.g. 'projects/brainsapp')."),
14
+ maxPages: z
15
+ .number()
16
+ .optional()
17
+ .describe("Maximum number of candidate pages to analyze for missing Related sections. Default 20, max 50."),
18
+ });
19
+ const MAX_SUGGESTIONS_PER_PAGE = 5;
20
+ const DEFAULT_MAX_PAGES = 20;
21
+ const HARD_MAX_PAGES = 50;
22
+ function hasRelatedPagesSection(content) {
23
+ return /^## Related pages/im.test(content);
24
+ }
25
+ /** Priority score for display ordering: open-loop first, decisions second, canonical last. */
26
+ function priorityScore(name) {
27
+ if (name.includes("open-loop"))
28
+ return 0;
29
+ if (/(?:^|\/)decisions\//.test(name))
30
+ return 1;
31
+ if (/(?:^|\/)canonical\//.test(name))
32
+ return 2;
33
+ return 3;
34
+ }
35
+ /** Build a root-relative wiki link path from a page name. */
36
+ function pageToLink(name, title) {
37
+ const withoutExt = name.endsWith(".md") ? name.slice(0, -3) : name;
38
+ return `- [${title}](/${withoutExt})`;
39
+ }
40
+ /** Build the ## Related pages patch block for a page. */
41
+ function buildPatch(suggestions) {
42
+ const links = suggestions.map((s) => pageToLink(s.name, s.title)).join("\n");
43
+ return `\n## Related pages\n\n${links}`;
44
+ }
45
+ export async function suggestLinks(drive, input, userId = "") {
46
+ const tags = (input.tags ?? []).map((t) => t.trim()).filter(Boolean);
47
+ const folder = input.folder?.trim();
48
+ if (tags.length === 0 && !folder) {
49
+ throw new Error("At least one filter is required: tags or folder");
50
+ }
51
+ const maxPages = Math.min(input.maxPages ?? DEFAULT_MAX_PAGES, HARD_MAX_PAGES);
52
+ const pgIndex = getPostgresSearchIndex();
53
+ const useGraph = pgIndex.isConfigured();
54
+ // ── Step 1: collect candidate page metadata ─────────────────────────────────
55
+ const candidatePages = [];
56
+ if (useGraph) {
57
+ // Single-tag fast path through Postgres
58
+ const rows = await pgIndex.listPages({
59
+ tag: tags.length === 1 ? tags[0] : undefined,
60
+ userId,
61
+ });
62
+ for (const r of rows) {
63
+ if (r.name === "index.md" || r.name === "log.md")
64
+ continue;
65
+ if (folder) {
66
+ const prefix = folder.endsWith("/") ? folder : `${folder}/`;
67
+ if (!r.name.startsWith(prefix))
68
+ continue;
69
+ }
70
+ if (tags.length > 1 && !tags.some((t) => r.tags.includes(t)))
71
+ continue;
72
+ candidatePages.push({ name: r.name, title: r.title, summary: r.summary, tags: r.tags });
73
+ }
74
+ }
75
+ else {
76
+ // Storage scan fallback
77
+ const files = await drive.listFiles();
78
+ for (const file of files) {
79
+ if (!file.name.endsWith(".md"))
80
+ continue;
81
+ if (file.name === "index.md" || file.name === "log.md")
82
+ continue;
83
+ if (folder) {
84
+ const prefix = folder.endsWith("/") ? folder : `${folder}/`;
85
+ if (!file.name.startsWith(prefix))
86
+ continue;
87
+ }
88
+ try {
89
+ const f = await drive.readFile(file.name);
90
+ const { frontmatter } = parsePage(f.content);
91
+ const pageTags = frontmatter.tags ?? [];
92
+ if (tags.length > 0 && !tags.some((t) => pageTags.includes(t)))
93
+ continue;
94
+ candidatePages.push({
95
+ name: file.name,
96
+ title: String(frontmatter.title ?? file.name),
97
+ summary: String(frontmatter.summary ?? ""),
98
+ tags: pageTags,
99
+ });
100
+ }
101
+ catch {
102
+ // skip unparseable files
103
+ }
104
+ }
105
+ }
106
+ // Build a name→metadata lookup for suggestion resolution
107
+ const pageMap = new Map(candidatePages.map((p) => [p.name, p]));
108
+ // ── Step 2: sort by priority, then filter to those missing ## Related pages ──
109
+ candidatePages.sort((a, b) => priorityScore(a.name) - priorityScore(b.name) || a.name.localeCompare(b.name));
110
+ const toAnalyze = [];
111
+ for (const page of candidatePages) {
112
+ if (toAnalyze.length >= maxPages)
113
+ break;
114
+ let content;
115
+ try {
116
+ const f = await drive.readFile(page.name);
117
+ content = f.content;
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ if (hasRelatedPagesSection(content))
123
+ continue;
124
+ const existingLinks = new Set(extractWikiLinks(page.name, content).map((l) => l.target));
125
+ toAnalyze.push({ name: page.name, title: page.title, tags: page.tags, existingLinks });
126
+ }
127
+ // ── Step 3: resolve all unique page names referenced in graph suggestions ────
128
+ // Build a shared full-wiki metadata map (only if Postgres is available and we
129
+ // need to resolve page titles that are outside the candidate set).
130
+ let allPagesMap = null;
131
+ async function resolveTitle(name) {
132
+ const inCandidates = pageMap.get(name);
133
+ if (inCandidates)
134
+ return inCandidates;
135
+ if (useGraph) {
136
+ if (!allPagesMap) {
137
+ const rows = await pgIndex.listPages({ userId }).catch(() => []);
138
+ allPagesMap = new Map(rows.map((r) => [r.name, { title: r.title }]));
139
+ }
140
+ return allPagesMap.get(name) ?? null;
141
+ }
142
+ return null;
143
+ }
144
+ // ── Step 4: generate suggestions for each candidate ─────────────────────────
145
+ const patches = [];
146
+ for (const page of toAnalyze) {
147
+ const seen = new Set();
148
+ const suggestions = [];
149
+ if (useGraph) {
150
+ const [related, backlinks] = await Promise.all([
151
+ pgIndex.getRelatedPages(page.name, userId).catch(() => []),
152
+ pgIndex.getBacklinks(page.name, userId).catch(() => []),
153
+ ]);
154
+ for (const rel of related) {
155
+ if (suggestions.length >= MAX_SUGGESTIONS_PER_PAGE)
156
+ break;
157
+ if (seen.has(rel) || rel === page.name || page.existingLinks.has(rel))
158
+ continue;
159
+ const meta = await resolveTitle(rel);
160
+ if (meta) {
161
+ suggestions.push({ name: rel, title: meta.title, reason: "shared context" });
162
+ seen.add(rel);
163
+ }
164
+ }
165
+ for (const bl of backlinks) {
166
+ if (suggestions.length >= MAX_SUGGESTIONS_PER_PAGE)
167
+ break;
168
+ if (seen.has(bl) || bl === page.name || page.existingLinks.has(bl))
169
+ continue;
170
+ const meta = await resolveTitle(bl);
171
+ if (meta) {
172
+ suggestions.push({ name: bl, title: meta.title, reason: "links here" });
173
+ seen.add(bl);
174
+ }
175
+ }
176
+ }
177
+ // Tag-overlap fallback (runs in both modes; supplements graph results)
178
+ if (suggestions.length < MAX_SUGGESTIONS_PER_PAGE && page.tags.length > 0) {
179
+ for (const other of candidatePages) {
180
+ if (suggestions.length >= MAX_SUGGESTIONS_PER_PAGE)
181
+ break;
182
+ if (seen.has(other.name) || other.name === page.name || page.existingLinks.has(other.name))
183
+ continue;
184
+ const sharedTags = page.tags.filter((t) => other.tags.includes(t));
185
+ if (sharedTags.length > 0) {
186
+ suggestions.push({
187
+ name: other.name,
188
+ title: other.title,
189
+ reason: `shared tags: ${sharedTags.join(", ")}`,
190
+ });
191
+ seen.add(other.name);
192
+ }
193
+ }
194
+ }
195
+ if (suggestions.length > 0) {
196
+ patches.push({
197
+ page: page.name,
198
+ suggestedLinks: suggestions,
199
+ patch: buildPatch(suggestions),
200
+ });
201
+ }
202
+ }
203
+ const msg = patches.length === 0
204
+ ? "No link suggestions found. All analyzed pages either already have ## Related pages or no cross-references were identified."
205
+ : `Found ${patches.length} page(s) with link suggestions. Review each patch and apply with patch_page (append operation). No changes have been made — suggestions require explicit confirmation.`;
206
+ return {
207
+ pagesChecked: candidatePages.length,
208
+ pagesWithoutRelatedSection: toAnalyze.length,
209
+ pagesWithSuggestions: patches.length,
210
+ patches,
211
+ message: msg,
212
+ };
213
+ }
214
+ //# sourceMappingURL=suggestLinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggestLinks.js","sourceRoot":"","sources":["../../src/tools/suggestLinks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { type StorageClient } from "../storage/client.js";
3
+ export declare const updatePageInputSchema: z.ZodObject<{
4
+ name: z.ZodString;
5
+ fileId: z.ZodString;
6
+ revision: z.ZodString;
7
+ body: z.ZodString;
8
+ frontmatterPatch: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodUnknown>, Record<string, unknown>, unknown>>;
9
+ summary: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ fileId: string;
12
+ body: string;
13
+ name: string;
14
+ revision: string;
15
+ summary?: string | undefined;
16
+ frontmatterPatch?: Record<string, unknown> | undefined;
17
+ }, {
18
+ fileId: string;
19
+ body: string;
20
+ name: string;
21
+ revision: string;
22
+ summary?: string | undefined;
23
+ frontmatterPatch?: unknown;
24
+ }>;
25
+ export type UpdatePageInput = z.infer<typeof updatePageInputSchema>;
26
+ export declare function updatePage(drive: StorageClient, input: UpdatePageInput, userId?: string): Promise<object>;
27
+ //# sourceMappingURL=updatePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updatePage.d.ts","sourceRoot":"","sources":["../../src/tools/updatePage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,aAAa,EAA4B,MAAM,sBAAsB,CAAC;AAYpF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;EAYhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,wBAAsB,UAAU,CAC9B,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,eAAe,EACtB,MAAM,SAAK,GACV,OAAO,CAAC,MAAM,CAAC,CA+GjB"}
@@ -0,0 +1,118 @@
1
+ import { z } from "zod";
2
+ import { resolveIndexOwnerSubject } from "../storage/client.js";
3
+ import { parsePage, serializePage } from "../wiki/frontmatter.js";
4
+ import { getIngestRoutingWarnings } from "../wiki/ingestPolicy.js";
5
+ import { normalizeWikiPath } from "../wiki/path.js";
6
+ import { upsertIndexEntry } from "../wiki/index.js";
7
+ import { appendLogEntry } from "../wiki/log.js";
8
+ import { getPostgresSearchIndex } from "../search/postgresIndex.js";
9
+ import { assertPageSize } from "../wiki/storageLimits.js";
10
+ import { tryParseJson } from "./coerce.js";
11
+ import { dispatchPageWebhook } from "../webhooks/dispatch.js";
12
+ import { snapshotPage } from "./pageHistory.js";
13
+ export const updatePageInputSchema = z.object({
14
+ name: z.string().describe("Relative path of the page to update (e.g. 'projects/my-topic.md')"),
15
+ fileId: z.string().describe("Drive file ID — must match the file you read"),
16
+ revision: z
17
+ .string()
18
+ .describe("Revision from your last read_page call — used for conflict detection"),
19
+ body: z.string().describe("New full markdown body (without frontmatter)"),
20
+ frontmatterPatch: z
21
+ .preprocess(tryParseJson, z.record(z.unknown()))
22
+ .optional()
23
+ .describe("Optional partial frontmatter overrides (e.g. {status: 'archived'})"),
24
+ summary: z.string().optional().describe("Updated one-line summary for the index"),
25
+ });
26
+ export async function updatePage(drive, input, userId = "") {
27
+ const normalizedName = normalizeWikiPath(input.name);
28
+ const pgIndex = getPostgresSearchIndex();
29
+ // Cross-account sharing (BRA-381): attribute index/webhook writes to the
30
+ // true storage owner, not the raw authenticated actor.
31
+ const indexOwner = resolveIndexOwnerSubject(drive, normalizedName, userId);
32
+ // Hot path: read from the Postgres row (authoritative when configured) —
33
+ // matches patchPage.ts. Also lets us compare input.revision against the
34
+ // *current* row instead of trusting a possibly-stale Storage mirror.
35
+ const dbRow = await pgIndex.fetchPageRow(normalizedName, indexOwner).catch(() => null);
36
+ const existing = dbRow ?? await drive.readFile(normalizedName);
37
+ if (dbRow && dbRow.revision !== input.revision) {
38
+ // Someone else wrote this page after our caller last read it — refuse
39
+ // the blind overwrite rather than silently clobbering their change.
40
+ return {
41
+ success: false,
42
+ conflict: true,
43
+ name: normalizedName,
44
+ reason: "Page was modified by someone else since your last read_page call. Call read_page again to see the current content, then retry with the new revision or a merged body.",
45
+ yourRevision: input.revision,
46
+ currentRevision: dbRow.revision,
47
+ };
48
+ }
49
+ const { frontmatter } = parsePage(existing.content);
50
+ const mergedFrontmatter = {
51
+ ...frontmatter,
52
+ ...(input.frontmatterPatch ?? {}),
53
+ };
54
+ const routingWarnings = getIngestRoutingWarnings({
55
+ name: normalizedName,
56
+ title: String(mergedFrontmatter.title ?? normalizedName),
57
+ tags: mergedFrontmatter.tags ?? [],
58
+ body: input.body,
59
+ });
60
+ const newContent = serializePage(mergedFrontmatter, input.body);
61
+ assertPageSize(userId, normalizedName, newContent);
62
+ // Validate the content actually about to be written — not the pre-merge
63
+ // content being replaced — so returned warnings describe what read_page
64
+ // will see afterward (GH #464).
65
+ const { warnings } = parsePage(newContent);
66
+ await snapshotPage(drive, normalizedName, existing.content);
67
+ const writeResult = await drive.updateFile(input.fileId, newContent, input.revision);
68
+ // Update Postgres index — best-effort. Stores the FULL serialized content
69
+ // (frontmatter included), matching patchPage.ts's DB-primary convention —
70
+ // read_page/patch_page read this column back as the complete file.
71
+ pgIndex.upsertPage(normalizedName, mergedFrontmatter, newContent, writeResult.revision, writeResult.modifiedTime, indexOwner).catch(() => { });
72
+ // Fire-and-forget: the link graph is a derived index, and nothing in this
73
+ // response depends on it. Awaiting it put three sequential Supabase round
74
+ // trips (~350-525ms) on the caller's critical path purely to decide whether
75
+ // to attach a cosmetic warning (GH #424). Staleness is self-healing —
76
+ // links_extracted_at drives extract_links' catch-up pass.
77
+ pgIndex
78
+ .syncPageLinks(normalizedName, input.body, indexOwner)
79
+ .catch((err) => {
80
+ console.warn(`[links] sync failed for '${normalizedName}'`, err instanceof Error ? err.message : err);
81
+ });
82
+ upsertIndexEntry(drive, {
83
+ name: normalizedName,
84
+ title: String(mergedFrontmatter.title ?? normalizedName),
85
+ summary: input.summary ?? String(mergedFrontmatter.summary ?? ""),
86
+ updated: new Date().toISOString().slice(0, 10),
87
+ }).catch(() => { });
88
+ appendLogEntry(drive, {
89
+ type: "update",
90
+ title: String(mergedFrontmatter.title ?? normalizedName),
91
+ detail: `Updated ${normalizedName}`,
92
+ }, indexOwner).catch((err) => {
93
+ // Surfaced rather than swallowed: a silent stop here is how an
94
+ // oversized log.md would go unnoticed (GH #417).
95
+ console.warn("[log] append failed:", err instanceof Error ? err.message : err);
96
+ });
97
+ // Dispatch outbound webhooks — fire-and-forget, never blocks the write
98
+ dispatchPageWebhook(indexOwner, "update", {
99
+ name: normalizedName,
100
+ title: String(mergedFrontmatter.title ?? normalizedName),
101
+ tags: mergedFrontmatter.tags ?? [],
102
+ summary: input.summary ?? String(mergedFrontmatter.summary ?? ""),
103
+ at: writeResult.modifiedTime ?? new Date().toISOString(),
104
+ });
105
+ return {
106
+ success: true,
107
+ name: normalizedName,
108
+ fileId: writeResult.fileId,
109
+ revision: writeResult.revision,
110
+ modifiedTime: writeResult.modifiedTime,
111
+ bytesWritten: Buffer.byteLength(newContent, "utf8"),
112
+ warnings: [
113
+ ...warnings.map((w) => ({ field: w.field, message: w.message })),
114
+ ...routingWarnings,
115
+ ],
116
+ };
117
+ }
118
+ //# sourceMappingURL=updatePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updatePage.js","sourceRoot":"","sources":["../../src/tools/updatePage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * dispatch.ts
3
+ *
4
+ * Outbound webhook dispatch: pattern matching (glob/regex) + HMAC-SHA256
5
+ * signing + async fire-and-forget HTTP POST.
6
+ *
7
+ * Design:
8
+ * - One POST per matching rule per write operation (not per page).
9
+ * - Never blocks or fails the originating write.
10
+ * - Patterns are tested as minimatch globs first, then as RegExp.
11
+ */
12
+ import { type WebhookRule } from "./webhookRules.js";
13
+ export interface WebhookPayload {
14
+ event: "page.created" | "page.updated";
15
+ name: string;
16
+ title: string;
17
+ tags: string[];
18
+ summary: string;
19
+ at: string;
20
+ }
21
+ /**
22
+ * Returns true if `pageName` matches the given glob or regex pattern.
23
+ *
24
+ * Glob matching rules (subset of minimatch):
25
+ * * — any sequence of characters EXCEPT /
26
+ * ** — any sequence including /
27
+ * ? — single character except /
28
+ *
29
+ * If the pattern is enclosed in /…/ it is treated as a regex.
30
+ */
31
+ export declare function matchesPattern(pageName: string, pattern: string): boolean;
32
+ /** Returns true if the rule matches the event and at least one pattern matches pageName. */
33
+ export declare function ruleMatchesPage(rule: WebhookRule, event: "create" | "update", pageName: string): boolean;
34
+ /**
35
+ * Fire webhooks for a single page write. Async fire-and-forget — never throws.
36
+ */
37
+ export declare function dispatchPageWebhook(userId: string, event: "create" | "update", payload: Omit<WebhookPayload, "event">): void;
38
+ /**
39
+ * Fire webhooks for a bundle push (coalesced per rule).
40
+ * Accepts a list of (name, event) pairs and fires one POST per matching rule.
41
+ */
42
+ export declare function dispatchBundleWebhooks(userId: string, pages: Array<{
43
+ name: string;
44
+ event: "create" | "update";
45
+ title: string;
46
+ tags: string[];
47
+ summary: string;
48
+ }>): void;
49
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/webhooks/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,cAAc,GAAG,cAAc,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ;AAID;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAczE;AAuCD,4FAA4F;AAC5F,wBAAgB,eAAe,CAC7B,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,QAAQ,GAAG,QAAQ,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAGT;AA6BD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,QAAQ,GAAG,QAAQ,EAC1B,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GACrC,IAAI,CAcN;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GACzG,IAAI,CA0BN"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * dispatch.ts
3
+ *
4
+ * Outbound webhook dispatch: pattern matching (glob/regex) + HMAC-SHA256
5
+ * signing + async fire-and-forget HTTP POST.
6
+ *
7
+ * Design:
8
+ * - One POST per matching rule per write operation (not per page).
9
+ * - Never blocks or fails the originating write.
10
+ * - Patterns are tested as minimatch globs first, then as RegExp.
11
+ */
12
+ import { createHmac } from "crypto";
13
+ import { getEnabledWebhookRulesForUser } from "./webhookRules.js";
14
+ // ── Pattern matching ──────────────────────────────────────────────────────────
15
+ /**
16
+ * Returns true if `pageName` matches the given glob or regex pattern.
17
+ *
18
+ * Glob matching rules (subset of minimatch):
19
+ * * — any sequence of characters EXCEPT /
20
+ * ** — any sequence including /
21
+ * ? — single character except /
22
+ *
23
+ * If the pattern is enclosed in /…/ it is treated as a regex.
24
+ */
25
+ export function matchesPattern(pageName, pattern) {
26
+ // Regex shorthand: /pattern/ or /pattern/flags
27
+ const regexMatch = /^\/(.+)\/([gimsuy]*)$/.exec(pattern);
28
+ if (regexMatch) {
29
+ try {
30
+ const re = new RegExp(regexMatch[1], regexMatch[2]);
31
+ return re.test(pageName);
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ // Glob matching — hand-rolled to avoid adding minimatch as a dep
38
+ return globMatch(pageName, pattern);
39
+ }
40
+ function globMatch(str, pattern) {
41
+ // Convert glob to regex
42
+ let reStr = "^";
43
+ let i = 0;
44
+ while (i < pattern.length) {
45
+ const ch = pattern[i];
46
+ if (ch === "*") {
47
+ if (pattern[i + 1] === "*") {
48
+ // ** — match everything including /
49
+ reStr += ".*";
50
+ i += 2;
51
+ // consume optional trailing /
52
+ if (pattern[i] === "/")
53
+ i++;
54
+ continue;
55
+ }
56
+ // * — match anything except /
57
+ reStr += "[^/]*";
58
+ i++;
59
+ continue;
60
+ }
61
+ if (ch === "?") {
62
+ reStr += "[^/]";
63
+ i++;
64
+ continue;
65
+ }
66
+ // Escape regex special chars
67
+ reStr += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
68
+ i++;
69
+ }
70
+ reStr += "$";
71
+ try {
72
+ return new RegExp(reStr).test(str);
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /** Returns true if the rule matches the event and at least one pattern matches pageName. */
79
+ export function ruleMatchesPage(rule, event, pageName) {
80
+ if (!rule.events.includes(event))
81
+ return false;
82
+ return rule.patterns.some((p) => matchesPattern(pageName, p));
83
+ }
84
+ // ── HMAC signing ──────────────────────────────────────────────────────────────
85
+ function signPayload(body, secret) {
86
+ return createHmac("sha256", secret).update(body).digest("hex");
87
+ }
88
+ // ── HTTP dispatch ─────────────────────────────────────────────────────────────
89
+ async function sendWebhook(rule, payload) {
90
+ const body = JSON.stringify(payload);
91
+ const headers = {
92
+ "Content-Type": "application/json",
93
+ "User-Agent": "brains-webhooks/1.0",
94
+ };
95
+ if (rule.secret) {
96
+ headers["X-Brains-Signature"] = "sha256=" + signPayload(body, rule.secret);
97
+ }
98
+ await fetch(rule.url, {
99
+ method: "POST",
100
+ headers,
101
+ body,
102
+ signal: AbortSignal.timeout(10000),
103
+ });
104
+ }
105
+ // ── Public dispatch helpers ───────────────────────────────────────────────────
106
+ /**
107
+ * Fire webhooks for a single page write. Async fire-and-forget — never throws.
108
+ */
109
+ export function dispatchPageWebhook(userId, event, payload) {
110
+ if (!userId)
111
+ return;
112
+ setImmediate(async () => {
113
+ try {
114
+ const rules = await getEnabledWebhookRulesForUser(userId);
115
+ for (const rule of rules) {
116
+ if (ruleMatchesPage(rule, event, payload.name)) {
117
+ await sendWebhook(rule, { ...payload, event: event === "create" ? "page.created" : "page.updated" }).catch(() => { });
118
+ }
119
+ }
120
+ }
121
+ catch {
122
+ // non-fatal
123
+ }
124
+ });
125
+ }
126
+ /**
127
+ * Fire webhooks for a bundle push (coalesced per rule).
128
+ * Accepts a list of (name, event) pairs and fires one POST per matching rule.
129
+ */
130
+ export function dispatchBundleWebhooks(userId, pages) {
131
+ if (!userId || pages.length === 0)
132
+ return;
133
+ const at = new Date().toISOString();
134
+ setImmediate(async () => {
135
+ try {
136
+ const rules = await getEnabledWebhookRulesForUser(userId);
137
+ for (const rule of rules) {
138
+ const matching = pages.filter((p) => ruleMatchesPage(rule, p.event, p.name));
139
+ if (matching.length === 0)
140
+ continue;
141
+ // Coalesce: one POST per rule, payload = first matching page
142
+ const first = matching[0];
143
+ const webhookEvent = first.event === "create" ? "page.created" : "page.updated";
144
+ await sendWebhook(rule, {
145
+ event: webhookEvent,
146
+ name: first.name,
147
+ title: first.title,
148
+ tags: first.tags,
149
+ summary: first.summary,
150
+ at,
151
+ }).catch(() => { });
152
+ }
153
+ }
154
+ catch {
155
+ // non-fatal
156
+ }
157
+ });
158
+ }
159
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../src/webhooks/dispatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * webhookRules.ts
3
+ *
4
+ * Postgres CRUD for user-configured outbound webhook rules.
5
+ * Table schema: scripts/webhooks-migration.sql
6
+ */
7
+ export interface WebhookRule {
8
+ id: string;
9
+ userId: string;
10
+ name: string;
11
+ url: string;
12
+ secret: string;
13
+ events: string[];
14
+ patterns: string[];
15
+ enabled: boolean;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ }
19
+ export declare function listWebhookRules(userId: string): Promise<WebhookRule[]>;
20
+ export declare function getWebhookRule(id: string, userId: string): Promise<WebhookRule | null>;
21
+ export declare function upsertWebhookRule(userId: string, rule: {
22
+ id?: string;
23
+ name: string;
24
+ url: string;
25
+ secret?: string;
26
+ events?: string[];
27
+ patterns?: string[];
28
+ enabled?: boolean;
29
+ }): Promise<WebhookRule>;
30
+ export declare function deleteWebhookRule(id: string, userId: string): Promise<boolean>;
31
+ export declare function addPatternToRule(id: string, userId: string, pattern: string): Promise<WebhookRule | null>;
32
+ /** Returns enabled rules for a user that have at least one pattern configured. */
33
+ export declare function getEnabledWebhookRulesForUser(userId: string): Promise<WebhookRule[]>;
34
+ //# sourceMappingURL=webhookRules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhookRules.d.ts","sourceRoot":"","sources":["../../src/webhooks/webhookRules.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAkDD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAc7E;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAW5F;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IACJ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GACA,OAAO,CAAC,WAAW,CAAC,CA+BtB;AAED,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUpF;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAS7B;AAED,kFAAkF;AAClF,wBAAsB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAG1F"}