@kinqs/brainrouter-mcp-server 0.3.4

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 (337) hide show
  1. package/.env.example +144 -0
  2. package/README.md +56 -0
  3. package/agents/README.md +120 -0
  4. package/agents/code-reviewer.md +97 -0
  5. package/agents/security-auditor.md +101 -0
  6. package/agents/test-engineer.md +95 -0
  7. package/dist/__tests__/agent_mode.test.d.ts +1 -0
  8. package/dist/__tests__/api-routes.test.d.ts +1 -0
  9. package/dist/__tests__/api-routes.test.js +170 -0
  10. package/dist/__tests__/crypto.test.d.ts +1 -0
  11. package/dist/__tests__/crypto.test.js +28 -0
  12. package/dist/__tests__/host-integrations.test.d.ts +1 -0
  13. package/dist/__tests__/host-integrations.test.js +82 -0
  14. package/dist/__tests__/integration.test.d.ts +1 -0
  15. package/dist/__tests__/integration.test.js +50 -0
  16. package/dist/__tests__/loader.test.d.ts +1 -0
  17. package/dist/__tests__/loader.test.js +89 -0
  18. package/dist/__tests__/neural-spark.test.d.ts +1 -0
  19. package/dist/__tests__/neural-spark.test.js +112 -0
  20. package/dist/__tests__/pagination.test.d.ts +1 -0
  21. package/dist/__tests__/pagination.test.js +23 -0
  22. package/dist/__tests__/redaction.test.d.ts +1 -0
  23. package/dist/__tests__/redaction.test.js +17 -0
  24. package/dist/__tests__/registry.test.d.ts +1 -0
  25. package/dist/__tests__/registry.test.js +56 -0
  26. package/dist/__tests__/retry.test.d.ts +1 -0
  27. package/dist/__tests__/retry.test.js +30 -0
  28. package/dist/__tests__/skill-activation.test.d.ts +1 -0
  29. package/dist/__tests__/skill-activation.test.js +112 -0
  30. package/dist/__tests__/working-memory.test.d.ts +1 -0
  31. package/dist/__tests__/working-memory.test.js +200 -0
  32. package/dist/__tests__/workspace-paths.test.d.ts +1 -0
  33. package/dist/__tests__/workspace-paths.test.js +56 -0
  34. package/dist/__tests__/writer.test.d.ts +1 -0
  35. package/dist/__tests__/writer.test.js +94 -0
  36. package/dist/api/auth/crypto.d.ts +4 -0
  37. package/dist/api/auth/crypto.js +54 -0
  38. package/dist/api/middleware/auth.d.ts +12 -0
  39. package/dist/api/middleware/auth.js +90 -0
  40. package/dist/api/pagination.d.ts +18 -0
  41. package/dist/api/pagination.js +32 -0
  42. package/dist/api/routes/auth.d.ts +1 -0
  43. package/dist/api/routes/auth.js +130 -0
  44. package/dist/api/routes/chat-completions.d.ts +7 -0
  45. package/dist/api/routes/chat-completions.js +474 -0
  46. package/dist/api/routes/contradictions.d.ts +1 -0
  47. package/dist/api/routes/contradictions.js +28 -0
  48. package/dist/api/routes/evidence.d.ts +1 -0
  49. package/dist/api/routes/evidence.js +59 -0
  50. package/dist/api/routes/governance.d.ts +1 -0
  51. package/dist/api/routes/governance.js +95 -0
  52. package/dist/api/routes/graph.d.ts +1 -0
  53. package/dist/api/routes/graph.js +25 -0
  54. package/dist/api/routes/hooks.d.ts +1 -0
  55. package/dist/api/routes/hooks.js +88 -0
  56. package/dist/api/routes/memories.d.ts +1 -0
  57. package/dist/api/routes/memories.js +92 -0
  58. package/dist/api/routes/persona.d.ts +1 -0
  59. package/dist/api/routes/persona.js +9 -0
  60. package/dist/api/routes/scenes.d.ts +1 -0
  61. package/dist/api/routes/scenes.js +35 -0
  62. package/dist/api/routes/skills.d.ts +1 -0
  63. package/dist/api/routes/skills.js +14 -0
  64. package/dist/api/routes/stats.d.ts +1 -0
  65. package/dist/api/routes/stats.js +8 -0
  66. package/dist/api/routes/users.d.ts +1 -0
  67. package/dist/api/routes/users.js +82 -0
  68. package/dist/api/routes/working.d.ts +1 -0
  69. package/dist/api/routes/working.js +88 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +492 -0
  72. package/dist/integrations/claude-code.d.ts +12 -0
  73. package/dist/integrations/claude-code.js +35 -0
  74. package/dist/integrations/codex.d.ts +12 -0
  75. package/dist/integrations/codex.js +34 -0
  76. package/dist/integrations/generic-mcp.d.ts +52 -0
  77. package/dist/integrations/generic-mcp.js +118 -0
  78. package/dist/loader.d.ts +29 -0
  79. package/dist/loader.js +200 -0
  80. package/dist/memory/capture.d.ts +35 -0
  81. package/dist/memory/capture.js +230 -0
  82. package/dist/memory/config.d.ts +2 -0
  83. package/dist/memory/config.js +3 -0
  84. package/dist/memory/engine.d.ts +203 -0
  85. package/dist/memory/engine.js +626 -0
  86. package/dist/memory/llm-semaphore.d.ts +41 -0
  87. package/dist/memory/llm-semaphore.js +81 -0
  88. package/dist/memory/memory-type-config.d.ts +11 -0
  89. package/dist/memory/memory-type-config.js +65 -0
  90. package/dist/memory/pipeline/cognitive-contradiction.d.ts +7 -0
  91. package/dist/memory/pipeline/cognitive-contradiction.js +59 -0
  92. package/dist/memory/pipeline/cognitive-dedup.d.ts +23 -0
  93. package/dist/memory/pipeline/cognitive-dedup.js +38 -0
  94. package/dist/memory/pipeline/cognitive-extractor.d.ts +21 -0
  95. package/dist/memory/pipeline/cognitive-extractor.js +183 -0
  96. package/dist/memory/pipeline/contextual-focus-builder.d.ts +13 -0
  97. package/dist/memory/pipeline/contextual-focus-builder.js +135 -0
  98. package/dist/memory/pipeline/focus-direction-shift.d.ts +10 -0
  99. package/dist/memory/pipeline/focus-direction-shift.js +27 -0
  100. package/dist/memory/pipeline/graph-builder.d.ts +11 -0
  101. package/dist/memory/pipeline/graph-builder.js +88 -0
  102. package/dist/memory/pipeline/graph-recall.d.ts +13 -0
  103. package/dist/memory/pipeline/graph-recall.js +55 -0
  104. package/dist/memory/pipeline/identity-distiller.d.ts +15 -0
  105. package/dist/memory/pipeline/identity-distiller.js +40 -0
  106. package/dist/memory/pipeline/l1-contradiction.d.ts +7 -0
  107. package/dist/memory/pipeline/l1-contradiction.js +66 -0
  108. package/dist/memory/pipeline/l1-dedup.d.ts +23 -0
  109. package/dist/memory/pipeline/l1-dedup.js +39 -0
  110. package/dist/memory/pipeline/l1-extractor.d.ts +21 -0
  111. package/dist/memory/pipeline/l1-extractor.js +180 -0
  112. package/dist/memory/pipeline/l2-direction-shift.d.ts +10 -0
  113. package/dist/memory/pipeline/l2-direction-shift.js +27 -0
  114. package/dist/memory/pipeline/l2-scene.d.ts +15 -0
  115. package/dist/memory/pipeline/l2-scene.js +140 -0
  116. package/dist/memory/pipeline/l3-distiller.d.ts +15 -0
  117. package/dist/memory/pipeline/l3-distiller.js +40 -0
  118. package/dist/memory/pipeline/neural-spark.d.ts +27 -0
  119. package/dist/memory/pipeline/neural-spark.js +78 -0
  120. package/dist/memory/pipeline/skill-prewarm.d.ts +63 -0
  121. package/dist/memory/pipeline/skill-prewarm.js +127 -0
  122. package/dist/memory/pipeline/task-queue.d.ts +54 -0
  123. package/dist/memory/pipeline/task-queue.js +117 -0
  124. package/dist/memory/prompts/cognitive-contradiction.d.ts +1 -0
  125. package/dist/memory/prompts/cognitive-contradiction.js +25 -0
  126. package/dist/memory/prompts/cognitive-extraction.d.ts +10 -0
  127. package/dist/memory/prompts/cognitive-extraction.js +114 -0
  128. package/dist/memory/prompts/core-identity.d.ts +6 -0
  129. package/dist/memory/prompts/core-identity.js +60 -0
  130. package/dist/memory/prompts/focus-direction-shift.d.ts +5 -0
  131. package/dist/memory/prompts/focus-direction-shift.js +32 -0
  132. package/dist/memory/prompts/focus-scene-cluster.d.ts +2 -0
  133. package/dist/memory/prompts/focus-scene-cluster.js +33 -0
  134. package/dist/memory/prompts/focus-scene.d.ts +7 -0
  135. package/dist/memory/prompts/focus-scene.js +40 -0
  136. package/dist/memory/prompts/graph-extraction-batch.d.ts +14 -0
  137. package/dist/memory/prompts/graph-extraction-batch.js +54 -0
  138. package/dist/memory/prompts/graph-extraction.d.ts +2 -0
  139. package/dist/memory/prompts/graph-extraction.js +53 -0
  140. package/dist/memory/prompts/l1-contradiction-batch.d.ts +16 -0
  141. package/dist/memory/prompts/l1-contradiction-batch.js +47 -0
  142. package/dist/memory/prompts/l1-contradiction.d.ts +1 -0
  143. package/dist/memory/prompts/l1-contradiction.js +25 -0
  144. package/dist/memory/prompts/l1-extraction.d.ts +10 -0
  145. package/dist/memory/prompts/l1-extraction.js +114 -0
  146. package/dist/memory/prompts/l2-direction-shift.d.ts +5 -0
  147. package/dist/memory/prompts/l2-direction-shift.js +32 -0
  148. package/dist/memory/prompts/l2-scene-cluster.d.ts +2 -0
  149. package/dist/memory/prompts/l2-scene-cluster.js +33 -0
  150. package/dist/memory/prompts/l2-scene.d.ts +7 -0
  151. package/dist/memory/prompts/l2-scene.js +40 -0
  152. package/dist/memory/prompts/l3-persona.d.ts +6 -0
  153. package/dist/memory/prompts/l3-persona.js +60 -0
  154. package/dist/memory/recall.d.ts +47 -0
  155. package/dist/memory/recall.js +427 -0
  156. package/dist/memory/redaction.d.ts +1 -0
  157. package/dist/memory/redaction.js +24 -0
  158. package/dist/memory/retry.d.ts +13 -0
  159. package/dist/memory/retry.js +53 -0
  160. package/dist/memory/scheduler.d.ts +9 -0
  161. package/dist/memory/scheduler.js +16 -0
  162. package/dist/memory/skill-hints-loader.d.ts +30 -0
  163. package/dist/memory/skill-hints-loader.js +100 -0
  164. package/dist/memory/store/embedding.d.ts +16 -0
  165. package/dist/memory/store/embedding.js +68 -0
  166. package/dist/memory/store/reranker.d.ts +24 -0
  167. package/dist/memory/store/reranker.js +83 -0
  168. package/dist/memory/store/sqlite.d.ts +167 -0
  169. package/dist/memory/store/sqlite.js +1816 -0
  170. package/dist/memory/store/types.d.ts +101 -0
  171. package/dist/memory/store/types.js +1 -0
  172. package/dist/memory/types.d.ts +207 -0
  173. package/dist/memory/types.js +7 -0
  174. package/dist/memory/validation.d.ts +441 -0
  175. package/dist/memory/validation.js +129 -0
  176. package/dist/memory/working/canvas.d.ts +5 -0
  177. package/dist/memory/working/canvas.js +43 -0
  178. package/dist/memory/working/offload.d.ts +71 -0
  179. package/dist/memory/working/offload.js +211 -0
  180. package/dist/memory/working/step-log.d.ts +16 -0
  181. package/dist/memory/working/step-log.js +35 -0
  182. package/dist/registry.d.ts +34 -0
  183. package/dist/registry.js +305 -0
  184. package/dist/resolver.d.ts +17 -0
  185. package/dist/resolver.js +126 -0
  186. package/dist/scripts/validate-foreign-workspace-path.d.ts +1 -0
  187. package/dist/scripts/validate-foreign-workspace-path.js +39 -0
  188. package/dist/tools/agent_memory_tools.d.ts +485 -0
  189. package/dist/tools/agent_memory_tools.js +793 -0
  190. package/dist/tools/create_skill.d.ts +46 -0
  191. package/dist/tools/create_skill.js +46 -0
  192. package/dist/tools/get_doc.d.ts +21 -0
  193. package/dist/tools/get_doc.js +24 -0
  194. package/dist/tools/get_persona.d.ts +15 -0
  195. package/dist/tools/get_persona.js +20 -0
  196. package/dist/tools/get_reference.d.ts +15 -0
  197. package/dist/tools/get_reference.js +20 -0
  198. package/dist/tools/get_skill.d.ts +34 -0
  199. package/dist/tools/get_skill.js +65 -0
  200. package/dist/tools/get_template_doc.d.ts +21 -0
  201. package/dist/tools/get_template_doc.js +24 -0
  202. package/dist/tools/list_docs.d.ts +15 -0
  203. package/dist/tools/list_docs.js +16 -0
  204. package/dist/tools/list_skills.d.ts +18 -0
  205. package/dist/tools/list_skills.js +17 -0
  206. package/dist/tools/list_template_docs.d.ts +15 -0
  207. package/dist/tools/list_template_docs.js +16 -0
  208. package/dist/tools/memory-engineering.d.ts +225 -0
  209. package/dist/tools/memory-engineering.js +284 -0
  210. package/dist/tools/memory-explain.d.ts +34 -0
  211. package/dist/tools/memory-explain.js +109 -0
  212. package/dist/tools/memory-governance.d.ts +171 -0
  213. package/dist/tools/memory-governance.js +224 -0
  214. package/dist/tools/memory-hooks.d.ts +67 -0
  215. package/dist/tools/memory-hooks.js +102 -0
  216. package/dist/tools/memory-working.d.ts +98 -0
  217. package/dist/tools/memory-working.js +101 -0
  218. package/dist/tools/memory_capture_turn.d.ts +66 -0
  219. package/dist/tools/memory_capture_turn.js +85 -0
  220. package/dist/tools/memory_consolidate.d.ts +55 -0
  221. package/dist/tools/memory_consolidate.js +176 -0
  222. package/dist/tools/memory_contradictions.d.ts +53 -0
  223. package/dist/tools/memory_contradictions.js +52 -0
  224. package/dist/tools/memory_graph_query.d.ts +51 -0
  225. package/dist/tools/memory_graph_query.js +35 -0
  226. package/dist/tools/memory_mark_cited.d.ts +43 -0
  227. package/dist/tools/memory_mark_cited.js +63 -0
  228. package/dist/tools/memory_recall.d.ts +77 -0
  229. package/dist/tools/memory_recall.js +81 -0
  230. package/dist/tools/memory_register_skill_hints.d.ts +49 -0
  231. package/dist/tools/memory_register_skill_hints.js +55 -0
  232. package/dist/tools/memory_resolve_session.d.ts +24 -0
  233. package/dist/tools/memory_resolve_session.js +133 -0
  234. package/dist/tools/memory_search.d.ts +146 -0
  235. package/dist/tools/memory_search.js +84 -0
  236. package/dist/tools/search_skills.d.ts +18 -0
  237. package/dist/tools/search_skills.js +17 -0
  238. package/dist/tools/update_doc.d.ts +24 -0
  239. package/dist/tools/update_doc.js +35 -0
  240. package/dist/tools/update_skill.d.ts +30 -0
  241. package/dist/tools/update_skill.js +80 -0
  242. package/dist/types.d.ts +81 -0
  243. package/dist/types.js +4 -0
  244. package/dist/writer.d.ts +30 -0
  245. package/dist/writer.js +220 -0
  246. package/docs/TEMPLATE ONLY +1 -0
  247. package/docs/api/API.md +64 -0
  248. package/docs/api/security/SECURITY.md +58 -0
  249. package/docs/deployment/DockerDeployment.md +30 -0
  250. package/docs/design/Design.md +59 -0
  251. package/docs/design/themes/apple.md +101 -0
  252. package/docs/design/themes/dieter-grid.md +100 -0
  253. package/docs/design/themes/gallery-white.md +100 -0
  254. package/docs/design/themes/pinterest.md +101 -0
  255. package/docs/design/themes/realty-open-house.md +101 -0
  256. package/docs/design/themes/vodafone.md +101 -0
  257. package/docs/hooks/Hooks.md +30 -0
  258. package/docs/schema/Schema.md +35 -0
  259. package/docs/strategy/ScalingStrategy.md +19 -0
  260. package/package.json +88 -0
  261. package/references/accessibility-checklist.md +160 -0
  262. package/references/orchestration-patterns.md +370 -0
  263. package/references/performance-checklist.md +153 -0
  264. package/references/security-checklist.md +134 -0
  265. package/references/testing-patterns.md +236 -0
  266. package/skills/agent/adr-skill/SKILL.md +299 -0
  267. package/skills/agent/agentic-engineering-workflow/SKILL.md +95 -0
  268. package/skills/agent/bootstrap-skill/SKILL.md +103 -0
  269. package/skills/agent/context-engineering/SKILL.md +307 -0
  270. package/skills/agent/debugging-and-error-recovery/SKILL.md +308 -0
  271. package/skills/agent/developer-growth-analysis/SKILL.md +328 -0
  272. package/skills/agent/doubt-driven-skill/SKILL.md +249 -0
  273. package/skills/agent/handover-skill/SKILL.md +112 -0
  274. package/skills/agent/idea-refine-skill/SKILL.md +185 -0
  275. package/skills/agent/idea-refine-skill/examples.md +238 -0
  276. package/skills/agent/idea-refine-skill/frameworks.md +99 -0
  277. package/skills/agent/idea-refine-skill/refinement-criteria.md +113 -0
  278. package/skills/agent/interview-skill/SKILL.md +226 -0
  279. package/skills/agent/planning-skill/SKILL.md +270 -0
  280. package/skills/agent/skill-authoring/SKILL.md +189 -0
  281. package/skills/agent/source-driven-skill/SKILL.md +197 -0
  282. package/skills/agent/spec-driven-skill/SKILL.md +221 -0
  283. package/skills/agent/sync-skill/SKILL.md +92 -0
  284. package/skills/agent/using-agent-skills/SKILL.md +189 -0
  285. package/skills/api/a11y-skill/SKILL.md +88 -0
  286. package/skills/api/api-skill/SKILL.md +123 -0
  287. package/skills/api/auth-skill/SKILL.md +80 -0
  288. package/skills/api/debug-skill/SKILL.md +535 -0
  289. package/skills/api/performance-skill/SKILL.md +100 -0
  290. package/skills/api/testing-skill/SKILL.md +100 -0
  291. package/skills/codebase/code-review-and-quality/SKILL.md +228 -0
  292. package/skills/codebase/code-simplification/SKILL.md +352 -0
  293. package/skills/codebase/code-structure-cleanup/SKILL.md +142 -0
  294. package/skills/codebase/concerns-skill/SKILL.md +89 -0
  295. package/skills/codebase/conventions-skill/SKILL.md +95 -0
  296. package/skills/codebase/doc-management-skill/SKILL.md +47 -0
  297. package/skills/codebase/git-workflow-skill/SKILL.md +312 -0
  298. package/skills/communication/1-3-1-rule/SKILL.md +120 -0
  299. package/skills/design/brutalist-skill/SKILL.md +131 -0
  300. package/skills/design/concept-diagrams/SKILL.md +387 -0
  301. package/skills/design/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
  302. package/skills/design/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
  303. package/skills/design/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
  304. package/skills/design/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
  305. package/skills/design/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
  306. package/skills/design/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
  307. package/skills/design/concept-diagrams/examples/electricity-grid-flow.md +182 -0
  308. package/skills/design/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
  309. package/skills/design/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
  310. package/skills/design/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
  311. package/skills/design/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
  312. package/skills/design/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
  313. package/skills/design/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
  314. package/skills/design/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
  315. package/skills/design/concept-diagrams/examples/wind-turbine-structure.md +338 -0
  316. package/skills/design/concept-diagrams/references/dashboard-patterns.md +43 -0
  317. package/skills/design/concept-diagrams/references/infrastructure-patterns.md +144 -0
  318. package/skills/design/concept-diagrams/references/physical-shape-cookbook.md +42 -0
  319. package/skills/design/concept-diagrams/templates/template.html +174 -0
  320. package/skills/design/gpt-tasteskill/SKILL.md +114 -0
  321. package/skills/design/minimalist-skill/SKILL.md +116 -0
  322. package/skills/design/output-skill/SKILL.md +87 -0
  323. package/skills/design/redesign-skill/SKILL.md +213 -0
  324. package/skills/design/soft-skill/SKILL.md +132 -0
  325. package/skills/design/stitch-skill/EXAMPLE.md +121 -0
  326. package/skills/design/stitch-skill/SKILL.md +222 -0
  327. package/skills/design/taste-skill/SKILL.md +269 -0
  328. package/skills/devops/ci-cd-skill/SKILL.md +402 -0
  329. package/skills/devops/docker-skill/SKILL.md +297 -0
  330. package/skills/devops/domain-skill/SKILL.md +234 -0
  331. package/skills/lifecycle/changelog-generator/SKILL.md +135 -0
  332. package/skills/lifecycle/incremental-skill/SKILL.md +257 -0
  333. package/skills/lifecycle/migration-skill/SKILL.md +218 -0
  334. package/skills/lifecycle/shipping-skill/SKILL.md +321 -0
  335. package/skills/memory/agent-memory/SKILL.md +122 -0
  336. package/skills/qa/browser-testing-skill/SKILL.md +314 -0
  337. package/skills/ux/adversarial-ux-skill/SKILL.md +168 -0
@@ -0,0 +1,474 @@
1
+ // OpenAI-compatible /v1/chat/completions endpoint, memory-augmented.
2
+ //
3
+ // What this gives us:
4
+ // - The MCP server speaks the OpenAI Chat Completions wire format so any
5
+ // OpenAI SDK or fetch-based client (the BrainRouter web chat, third-party
6
+ // tools, the CLI itself) can use it transparently.
7
+ // - Before forwarding the request to the upstream LLM, we run BrainRouter
8
+ // memory_recall and memory_working_context for the user and inject a
9
+ // compact "## BrainRouter Memory Briefing" system message at the front
10
+ // of the messages array. The user gets their own memory without lifting
11
+ // a finger — the entire point of building this.
12
+ // - After the upstream completes (streaming or not), we capture the turn
13
+ // via memoryEngine.capture(...). This is what makes System-2 learn over
14
+ // time.
15
+ //
16
+ // Auth: same Bearer header convention as the rest of the API
17
+ // (memory API key OR JWT, via requireAnyAuth).
18
+ //
19
+ // Upstream LLM: same env-driven config as the rest of the engine
20
+ // (BRAINROUTER_LLM_ENDPOINT / BRAINROUTER_LLM_API_KEY / BRAINROUTER_LLM_MODEL).
21
+ // We forward streaming requests as Server-Sent Events back to the client.
22
+ import { Router } from "express";
23
+ import { memoryEngine } from "../../memory/engine.js";
24
+ import { requireAnyAuth } from "../middleware/auth.js";
25
+ export const chatCompletionsRouter = Router();
26
+ chatCompletionsRouter.use(requireAnyAuth);
27
+ const DEFAULT_UPSTREAM_ENDPOINT = process.env.BRAINROUTER_LLM_ENDPOINT ?? "https://api.openai.com/v1/chat/completions";
28
+ function flattenContent(content) {
29
+ if (typeof content === "string")
30
+ return content;
31
+ if (!Array.isArray(content))
32
+ return "";
33
+ return content
34
+ .map((part) => (part && typeof part === "object" && "text" in part ? part.text ?? "" : ""))
35
+ .filter(Boolean)
36
+ .join("\n");
37
+ }
38
+ function buildBriefingMessage(briefing, sessionKey) {
39
+ return {
40
+ role: "system",
41
+ content: [
42
+ "## BrainRouter Memory Briefing",
43
+ `Session: ${sessionKey}`,
44
+ "",
45
+ briefing.trim(),
46
+ "",
47
+ "Cite the IDs of records you actually used in your reasoning.",
48
+ ].join("\n"),
49
+ };
50
+ }
51
+ /**
52
+ * Multi-source briefing. Pull whatever the BrainRouter brain already knows
53
+ * about the authenticated user and stitch it into one compact system message.
54
+ *
55
+ * Sources (each is best-effort; missing sources are silently skipped):
56
+ * - Core identity / persona (cross-session — this is the big cross-session win)
57
+ * - Top focus scenes (what the user has been working on lately)
58
+ * - Cognitive recall against the current query
59
+ * - Working memory canvas for the current session
60
+ *
61
+ * recall is already user-scoped at the FTS layer, so memories from CLI
62
+ * sessions surface here too — provided extraction has run on those sessions.
63
+ */
64
+ async function fetchBriefing(userId, sessionKey, query, activeSkill) {
65
+ const sections = [];
66
+ // 1. Persona (cross-session identity).
67
+ try {
68
+ const persona = memoryEngine.getPersona(userId);
69
+ const personaMd = persona?.personaMd?.toString().trim();
70
+ if (personaMd) {
71
+ sections.push(`### Who I'm talking to (core identity)\n${personaMd.slice(0, 1600)}`);
72
+ }
73
+ }
74
+ catch (err) {
75
+ console.error("[BrainRouter:/v1] persona briefing failed:", err);
76
+ }
77
+ // 2. Recent focus scenes.
78
+ try {
79
+ const scenes = memoryEngine.getTopScenes(userId, 3);
80
+ if (Array.isArray(scenes) && scenes.length > 0) {
81
+ const lines = ["### Recent focus scenes (what they've been working on)"];
82
+ for (const s of scenes) {
83
+ const heatScore = s.heatScore ?? "";
84
+ const name = s.sceneName ?? "";
85
+ const summary = (s.summary ?? "").toString().replace(/\s+/g, " ").slice(0, 220);
86
+ if (name)
87
+ lines.push(`- ${name}${heatScore !== "" ? ` · heat ${Number(heatScore).toFixed(2)}` : ""}: ${summary}`);
88
+ }
89
+ if (lines.length > 1)
90
+ sections.push(lines.join("\n"));
91
+ }
92
+ }
93
+ catch (err) {
94
+ console.error("[BrainRouter:/v1] scenes briefing failed:", err);
95
+ }
96
+ // 3. Cognitive recall against the query (FTS + vector + graph).
97
+ const recalledIds = new Set();
98
+ try {
99
+ const recall = await memoryEngine.recall({ userId, sessionKey, query, activeSkill });
100
+ const records = recall?.recalledCognitiveMemories ??
101
+ recall?.recalledCognitiveRecords ?? // legacy alias for old callers
102
+ [];
103
+ if (Array.isArray(records) && records.length > 0) {
104
+ const lines = ["### Recalled cognitive memories for this question"];
105
+ for (const r of records.slice(0, 10)) {
106
+ const id = (r.recordId ?? "").toString();
107
+ if (id)
108
+ recalledIds.add(id);
109
+ const content = (r.content ?? "").toString().replace(/\s+/g, " ").slice(0, 240);
110
+ lines.push(`- [${id}] (${r.type ?? "memory"}) ${content}`);
111
+ }
112
+ sections.push(lines.join("\n"));
113
+ }
114
+ }
115
+ catch (err) {
116
+ console.error("[BrainRouter:/v1] recall briefing failed:", err);
117
+ }
118
+ // 4. Recency-based memories — what we've been doing lately even when the
119
+ // user's query doesn't share keywords with cognitive content. This is
120
+ // what makes "what did we talk about last time?" / "remind me about the
121
+ // previous bug" actually work; FTS alone misses them.
122
+ try {
123
+ const recent = memoryEngine.store.listMemories(userId, { archived: false }, { limit: 8 }) ?? [];
124
+ const deduped = recent.filter((r) => !recalledIds.has((r.recordId ?? "").toString()));
125
+ if (deduped.length > 0) {
126
+ const lines = ["### Most recent memories (chronological, may or may not match the question)"];
127
+ for (const r of deduped.slice(0, 6)) {
128
+ const id = (r.recordId ?? "").toString();
129
+ const content = (r.content ?? "").toString().replace(/\s+/g, " ").slice(0, 240);
130
+ const when = (r.createdTime ?? "").toString().slice(0, 10);
131
+ lines.push(`- [${id}] (${r.type ?? "memory"}, ${when}) ${content}`);
132
+ }
133
+ sections.push(lines.join("\n"));
134
+ }
135
+ }
136
+ catch (err) {
137
+ console.error("[BrainRouter:/v1] recency briefing failed:", err);
138
+ }
139
+ return sections.join("\n\n");
140
+ }
141
+ /** Lightweight per-user memory counts for the chat status badge. */
142
+ export function getMemoryStatusForUser(userId) {
143
+ let cognitive = 0;
144
+ let scenes = 0;
145
+ let hasPersona = false;
146
+ try {
147
+ const stats = memoryEngine.store?.getMemoryStats?.(userId);
148
+ if (stats && typeof stats.total === "number")
149
+ cognitive = stats.total;
150
+ }
151
+ catch { /* ignore */ }
152
+ try {
153
+ const list = memoryEngine.getTopScenes(userId, 50);
154
+ if (Array.isArray(list))
155
+ scenes = list.length;
156
+ }
157
+ catch { /* ignore */ }
158
+ try {
159
+ const p = memoryEngine.getPersona(userId);
160
+ hasPersona = Boolean(p?.personaMd?.trim());
161
+ }
162
+ catch { /* ignore */ }
163
+ return { cognitive, scenes, hasPersona };
164
+ }
165
+ /**
166
+ * Record the exchange into BrainRouter memory.
167
+ *
168
+ * mode === "sensory" → cheap: just store sensory rows. No upstream LLM
169
+ * call. This is the default for the web chat so a
170
+ * single user message does NOT cascade into
171
+ * extraction + contradiction + persona + graph
172
+ * requests against the upstream model.
173
+ * mode === "full" → full pipeline: cognitive extraction, contradiction
174
+ * detection, persona distillation, graph build.
175
+ * Multiple upstream LLM calls per turn. Use this
176
+ * when the user explicitly asks for deep memory.
177
+ * mode === "off" → no-op.
178
+ */
179
+ async function captureTurn(userId, sessionKey, userText, assistantText, activeSkill, mode) {
180
+ if (mode === "off")
181
+ return;
182
+ if (!userText || !assistantText)
183
+ return;
184
+ try {
185
+ if (mode === "sensory") {
186
+ memoryEngine.capturePassiveL0({ userId, sessionKey, role: "user", content: userText, skillTag: activeSkill });
187
+ memoryEngine.capturePassiveL0({ userId, sessionKey, role: "assistant", content: assistantText, skillTag: activeSkill });
188
+ return;
189
+ }
190
+ await memoryEngine.capture({
191
+ userId,
192
+ sessionKey,
193
+ messages: [
194
+ { role: "user", content: userText, timestamp: Date.now() },
195
+ { role: "assistant", content: assistantText, timestamp: Date.now() },
196
+ ],
197
+ activeSkill,
198
+ });
199
+ }
200
+ catch (err) {
201
+ console.error("[BrainRouter:/v1] capture failed:", err);
202
+ }
203
+ }
204
+ function pickLastUserText(messages) {
205
+ for (let i = messages.length - 1; i >= 0; i--) {
206
+ if (messages[i].role === "user") {
207
+ return flattenContent(messages[i].content);
208
+ }
209
+ }
210
+ return "";
211
+ }
212
+ function sseLine(data) {
213
+ return `data: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`;
214
+ }
215
+ // Idle watchdog: if the upstream stops sending bytes for this long and we
216
+ // haven't received [DONE], we treat the stream as dead and close the client.
217
+ // This prevents the "requests keep coming nonstop" symptom that happens when
218
+ // an upstream server holds a keep-alive connection open after the SSE body
219
+ // is logically complete.
220
+ const STREAM_IDLE_TIMEOUT_MS = 30_000;
221
+ async function streamUpstream(upstreamRes, clientRes, onAssistantText) {
222
+ clientRes.setHeader("Content-Type", "text/event-stream");
223
+ clientRes.setHeader("Cache-Control", "no-cache");
224
+ clientRes.setHeader("Connection", "keep-alive");
225
+ clientRes.setHeader("X-Accel-Buffering", "no"); // disable nginx buffering if proxied
226
+ clientRes.flushHeaders?.();
227
+ const reader = upstreamRes.body?.getReader();
228
+ if (!reader) {
229
+ clientRes.end();
230
+ return;
231
+ }
232
+ let clientClosed = false;
233
+ let upstreamDone = false;
234
+ let idleTimer;
235
+ const armIdleTimer = () => {
236
+ if (idleTimer)
237
+ clearTimeout(idleTimer);
238
+ idleTimer = setTimeout(() => {
239
+ if (!upstreamDone) {
240
+ // Abandon the upstream and tell the client we're closing.
241
+ try {
242
+ void reader.cancel("idle-timeout");
243
+ }
244
+ catch { /* noop */ }
245
+ }
246
+ }, STREAM_IDLE_TIMEOUT_MS);
247
+ };
248
+ // If the browser tab closes mid-stream, the client TCP socket emits 'close'.
249
+ // Cancel the upstream reader so we don't keep pulling bytes from OpenAI.
250
+ clientRes.on("close", () => {
251
+ clientClosed = true;
252
+ if (idleTimer)
253
+ clearTimeout(idleTimer);
254
+ try {
255
+ void reader.cancel("client-closed");
256
+ }
257
+ catch { /* noop */ }
258
+ });
259
+ const decoder = new TextDecoder();
260
+ let buffer = "";
261
+ armIdleTimer();
262
+ try {
263
+ for (;;) {
264
+ if (clientClosed)
265
+ break;
266
+ const { value, done } = await reader.read();
267
+ if (done)
268
+ break;
269
+ armIdleTimer();
270
+ const chunk = decoder.decode(value, { stream: true });
271
+ buffer += chunk;
272
+ // Forward bytes as-is so client SSE parsing works.
273
+ clientRes.write(chunk);
274
+ // Sniff assistant content for capture, and detect [DONE].
275
+ const lines = buffer.split("\n");
276
+ buffer = lines.pop() ?? "";
277
+ for (const line of lines) {
278
+ const t = line.trim();
279
+ if (!t.startsWith("data:"))
280
+ continue;
281
+ const payload = t.slice(5).trim();
282
+ if (!payload)
283
+ continue;
284
+ if (payload === "[DONE]") {
285
+ upstreamDone = true;
286
+ // Explicitly stop reading after [DONE] — some upstreams keep the
287
+ // chunked connection open well past the logical end of the response,
288
+ // which is exactly the case that made the browser appear to "keep
289
+ // requesting" forever.
290
+ try {
291
+ void reader.cancel("done-sentinel");
292
+ }
293
+ catch { /* noop */ }
294
+ break;
295
+ }
296
+ try {
297
+ const obj = JSON.parse(payload);
298
+ const delta = obj?.choices?.[0]?.delta?.content;
299
+ if (typeof delta === "string")
300
+ onAssistantText(delta);
301
+ }
302
+ catch {
303
+ // ignore malformed delta lines
304
+ }
305
+ }
306
+ if (upstreamDone)
307
+ break;
308
+ }
309
+ }
310
+ catch (err) {
311
+ if (!clientClosed)
312
+ clientRes.write(sseLine({ error: { message: err?.message || "stream error" } }));
313
+ }
314
+ finally {
315
+ if (idleTimer)
316
+ clearTimeout(idleTimer);
317
+ if (!clientClosed) {
318
+ // Always emit a final [DONE] so the client's SSE parser sees a clean end
319
+ // even if the upstream didn't send one.
320
+ if (!upstreamDone)
321
+ clientRes.write("data: [DONE]\n\n");
322
+ clientRes.end();
323
+ }
324
+ }
325
+ }
326
+ chatCompletionsRouter.post("/chat/completions", async (req, res) => {
327
+ const userId = req.userId;
328
+ const body = (req.body ?? {});
329
+ if (!Array.isArray(body.messages) || body.messages.length === 0) {
330
+ res.status(400).json({ error: { message: "messages[] is required" } });
331
+ return;
332
+ }
333
+ const sessionKey = body.brainrouter?.sessionKey ??
334
+ req.headers["x-brainrouter-session"] ??
335
+ `web:${userId}`;
336
+ const activeSkill = body.brainrouter?.activeSkill;
337
+ const injectBriefing = body.brainrouter?.inject_briefing !== false;
338
+ // Capture mode. Default is "sensory" so a single chat turn NEVER triggers
339
+ // the heavy cognitive cascade (extraction + per-memory contradiction checks +
340
+ // graph build + persona distillation), each of which can fire its own upstream
341
+ // LLM call. That cascade is what was bombarding LM Studio with hundreds of
342
+ // queued requests. Run distillation explicitly via POST /v1/distill instead.
343
+ const captureMode = body.brainrouter?.capture_mode ??
344
+ (body.brainrouter?.capture_turn === false ? "off" : "sensory");
345
+ const lastUserText = pickLastUserText(body.messages);
346
+ // 1. Build memory briefing.
347
+ let outboundMessages = [...body.messages];
348
+ if (injectBriefing && lastUserText) {
349
+ const briefing = await fetchBriefing(userId, sessionKey, lastUserText, activeSkill);
350
+ if (briefing) {
351
+ // Place briefing immediately after any caller-provided system messages
352
+ // so it travels at the top of context without overwriting persona.
353
+ const insertAt = outboundMessages.findIndex((m) => m.role !== "system");
354
+ const briefMsg = buildBriefingMessage(briefing, sessionKey);
355
+ if (insertAt === -1)
356
+ outboundMessages.push(briefMsg);
357
+ else
358
+ outboundMessages.splice(insertAt, 0, briefMsg);
359
+ }
360
+ }
361
+ // 2. Forward to upstream.
362
+ const upstreamApiKey = process.env.BRAINROUTER_LLM_API_KEY;
363
+ if (!upstreamApiKey) {
364
+ res.status(503).json({
365
+ error: {
366
+ message: "Upstream LLM not configured. Set BRAINROUTER_LLM_API_KEY on the MCP server (or use the CLI which forwards it automatically).",
367
+ },
368
+ });
369
+ return;
370
+ }
371
+ const upstreamPayload = {
372
+ model: body.model ?? process.env.BRAINROUTER_LLM_MODEL ?? "gpt-4o-mini",
373
+ messages: outboundMessages.map((m) => ({ role: m.role, content: flattenContent(m.content), name: m.name })),
374
+ stream: Boolean(body.stream),
375
+ };
376
+ if (typeof body.temperature === "number")
377
+ upstreamPayload.temperature = body.temperature;
378
+ if (typeof body.max_tokens === "number")
379
+ upstreamPayload.max_tokens = body.max_tokens;
380
+ let upstream;
381
+ try {
382
+ upstream = await fetch(DEFAULT_UPSTREAM_ENDPOINT, {
383
+ method: "POST",
384
+ headers: {
385
+ "Content-Type": "application/json",
386
+ Authorization: `Bearer ${upstreamApiKey}`,
387
+ },
388
+ body: JSON.stringify(upstreamPayload),
389
+ });
390
+ }
391
+ catch (err) {
392
+ res.status(502).json({ error: { message: `Upstream fetch failed: ${err?.message || err}` } });
393
+ return;
394
+ }
395
+ if (!upstream.ok) {
396
+ const text = await upstream.text();
397
+ res.status(upstream.status).json({
398
+ error: { message: `Upstream returned ${upstream.status}: ${text.slice(0, 500)}` },
399
+ });
400
+ return;
401
+ }
402
+ // 3. Stream or buffer.
403
+ if (body.stream) {
404
+ let collected = "";
405
+ await streamUpstream(upstream, res, (delta) => { collected += delta; });
406
+ void captureTurn(userId, sessionKey, lastUserText, collected, activeSkill, captureMode);
407
+ return;
408
+ }
409
+ const json = (await upstream.json());
410
+ const assistantText = json?.choices?.[0]?.message?.content ?? "";
411
+ void captureTurn(userId, sessionKey, lastUserText, String(assistantText), activeSkill, captureMode);
412
+ res.json(json);
413
+ });
414
+ // ─── Distillation ────────────────────────────────────────────────────────────
415
+ // One in-flight cognitive extraction per user at a time. The pipeline cascades
416
+ // into contradiction / graph / persona work that each issue their own upstream
417
+ // LLM calls; without serialization, two clicks of the "Distill" button (or
418
+ // multiple browser tabs) can pile dozens of jobs onto the upstream queue.
419
+ const distillInFlight = new Map();
420
+ chatCompletionsRouter.post("/distill", async (req, res) => {
421
+ const userId = req.userId;
422
+ if (distillInFlight.has(userId)) {
423
+ res.status(202).json({ status: "already-running", message: "A distillation pass is already in flight for this user. Wait for it to finish." });
424
+ return;
425
+ }
426
+ const sessionKey = req.body?.sessionKey ??
427
+ req.headers["x-brainrouter-session"] ??
428
+ `web:${userId}`;
429
+ const work = (async () => {
430
+ try {
431
+ // capture() with an empty messages array still drains the existing
432
+ // unextracted sensory backlog and runs the threshold check. We DO want
433
+ // the cascade here because the user explicitly asked for it.
434
+ await memoryEngine.capture({ userId, sessionKey, messages: [] });
435
+ return { status: "ok" };
436
+ }
437
+ catch (err) {
438
+ return { status: "error", error: err?.message ?? String(err) };
439
+ }
440
+ })();
441
+ distillInFlight.set(userId, work);
442
+ work.finally(() => distillInFlight.delete(userId));
443
+ const result = await work;
444
+ res.json(result);
445
+ });
446
+ // Memory-status badge for the web chat: tells the user how much BrainRouter
447
+ // already knows about them (cognitive records + scenes + whether persona is
448
+ // distilled). Returning 0/0/false is the honest signal that the LLM truly has
449
+ // no cross-session context to draw on yet.
450
+ chatCompletionsRouter.get("/memory-status", (req, res) => {
451
+ const userId = req.userId;
452
+ res.json(getMemoryStatusForUser(userId));
453
+ });
454
+ // Minimal /v1/models so OpenAI SDK clients that list models don't 404.
455
+ chatCompletionsRouter.get("/models", (_req, res) => {
456
+ const defaultModel = process.env.BRAINROUTER_LLM_MODEL ?? "gpt-4o-mini";
457
+ res.json({
458
+ object: "list",
459
+ data: [
460
+ {
461
+ id: defaultModel,
462
+ object: "model",
463
+ created: Math.floor(Date.now() / 1000),
464
+ owned_by: "brainrouter",
465
+ },
466
+ {
467
+ id: "brainrouter-default",
468
+ object: "model",
469
+ created: Math.floor(Date.now() / 1000),
470
+ owned_by: "brainrouter",
471
+ },
472
+ ],
473
+ });
474
+ });
@@ -0,0 +1 @@
1
+ export declare const contradictionsRouter: import("express-serve-static-core").Router;
@@ -0,0 +1,28 @@
1
+ import { Router } from "express";
2
+ import { memoryEngine } from "../../memory/engine.js";
3
+ import { requireAnyAuth } from "../middleware/auth.js";
4
+ import { decodeCursor, pageItems, PaginationQuerySchema } from "../pagination.js";
5
+ export const contradictionsRouter = Router();
6
+ contradictionsRouter.use(requireAnyAuth);
7
+ contradictionsRouter.get("/", (req, res) => {
8
+ try {
9
+ const pagination = PaginationQuerySchema.parse(req.query);
10
+ const contradictions = memoryEngine.getPendingContradictions(req.userId, {
11
+ cursor: decodeCursor(pagination.cursor),
12
+ limit: pagination.limit + 1,
13
+ });
14
+ const page = pageItems(contradictions, pagination.limit, (contradiction) => ({
15
+ confidence: contradiction.confidence,
16
+ id: contradiction.id,
17
+ }));
18
+ res.json({ contradictions: page.items, nextCursor: page.nextCursor, limit: pagination.limit, hasMore: Boolean(page.nextCursor) });
19
+ }
20
+ catch (error) {
21
+ res.status(400).json({ error: error instanceof Error ? error.message : "Invalid pagination parameters" });
22
+ }
23
+ });
24
+ contradictionsRouter.post("/:id/resolve", (req, res) => {
25
+ const status = req.body?.status === "dismissed" ? "dismissed" : "resolved";
26
+ memoryEngine.resolveContradiction(String(req.params.id), req.userId, status);
27
+ res.json({ success: true });
28
+ });
@@ -0,0 +1 @@
1
+ export declare const evidenceRouter: import("express-serve-static-core").Router;
@@ -0,0 +1,59 @@
1
+ import { Router } from "express";
2
+ import { memoryEngine } from "../../memory/engine.js";
3
+ import { requireAnyAuth } from "../middleware/auth.js";
4
+ import { decodeCursor, pageItems, PaginationQuerySchema } from "../pagination.js";
5
+ export const evidenceRouter = Router();
6
+ evidenceRouter.use(requireAnyAuth);
7
+ function scopedUserId(req, requested) {
8
+ const requestedUserId = typeof requested === "string" && requested.trim() ? requested.trim() : undefined;
9
+ if (!requestedUserId || requestedUserId === req.userId)
10
+ return req.userId;
11
+ if (req.isAdmin)
12
+ return requestedUserId;
13
+ throw new Error("Cannot access another user's evidence");
14
+ }
15
+ /**
16
+ * GET /api/evidence
17
+ * Returns all evidence rows for the authenticated user.
18
+ * Optional query params:
19
+ * - recordId: filter by parent memory record
20
+ * - kind: filter by evidence kind (file, command, url, test, benchmark, memory, other)
21
+ * - limit: max results (default 20, max 100)
22
+ * - cursor: pagination cursor
23
+ */
24
+ evidenceRouter.get("/", async (req, res) => {
25
+ try {
26
+ const pagination = PaginationQuerySchema.parse(req.query);
27
+ const userId = scopedUserId(req, req.query.userId);
28
+ const filters = {
29
+ recordId: typeof req.query.recordId === "string" && req.query.recordId.trim() ? req.query.recordId.trim() : undefined,
30
+ kind: typeof req.query.kind === "string" && req.query.kind !== "all" ? req.query.kind : undefined,
31
+ };
32
+ const evidence = memoryEngine.listEvidence(userId, filters, {
33
+ cursor: decodeCursor(pagination.cursor),
34
+ limit: pagination.limit + 1,
35
+ });
36
+ const page = pageItems(evidence, pagination.limit, (ev) => ({
37
+ observedAt: ev.observedAt,
38
+ id: ev.id,
39
+ }));
40
+ res.json({ evidence: page.items, nextCursor: page.nextCursor, limit: pagination.limit, hasMore: Boolean(page.nextCursor) });
41
+ }
42
+ catch (error) {
43
+ res.status(400).json({ error: error instanceof Error ? error.message : "Invalid evidence parameters" });
44
+ }
45
+ });
46
+ /**
47
+ * GET /api/evidence/:recordId
48
+ * Returns all evidence attached to a specific memory record.
49
+ */
50
+ evidenceRouter.get("/:recordId", async (req, res) => {
51
+ try {
52
+ const recordId = String(req.params.recordId);
53
+ const evidence = memoryEngine.getEvidence(req.userId, recordId);
54
+ res.json({ evidence, total: evidence.length });
55
+ }
56
+ catch (error) {
57
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to fetch evidence" });
58
+ }
59
+ });
@@ -0,0 +1 @@
1
+ export declare const governanceRouter: import("express-serve-static-core").Router;
@@ -0,0 +1,95 @@
1
+ import { Router } from "express";
2
+ import { memoryEngine } from "../../memory/engine.js";
3
+ import { requireAnyAuth } from "../middleware/auth.js";
4
+ import { decodeCursor, pageItems, PaginationQuerySchema } from "../pagination.js";
5
+ export const governanceRouter = Router();
6
+ governanceRouter.use(requireAnyAuth);
7
+ function scopedUserId(req, requested) {
8
+ const requestedUserId = typeof requested === "string" && requested.trim() ? requested.trim() : undefined;
9
+ if (!requestedUserId || requestedUserId === req.userId)
10
+ return req.userId;
11
+ if (req.isAdmin)
12
+ return requestedUserId;
13
+ throw new Error("Cannot access another user's memory operations");
14
+ }
15
+ governanceRouter.get("/export", (req, res) => {
16
+ res.json(memoryEngine.exportMemories(req.userId));
17
+ });
18
+ governanceRouter.post("/import", (req, res) => {
19
+ try {
20
+ res.json(memoryEngine.importMemories(req.userId, req.body));
21
+ }
22
+ catch (error) {
23
+ res.status(400).json({ error: error instanceof Error ? error.message : "Invalid import payload" });
24
+ }
25
+ });
26
+ governanceRouter.get("/audit", (req, res) => {
27
+ try {
28
+ const pagination = PaginationQuerySchema.parse(req.query);
29
+ const operations = memoryEngine.getOperationLog(req.userId, {
30
+ cursor: decodeCursor(pagination.cursor),
31
+ limit: pagination.limit + 1,
32
+ });
33
+ const page = pageItems(operations, pagination.limit, (operation) => ({
34
+ createdAt: operation.createdAt,
35
+ id: operation.id,
36
+ }));
37
+ res.json({ operations: page.items, nextCursor: page.nextCursor, limit: pagination.limit, hasMore: Boolean(page.nextCursor) });
38
+ }
39
+ catch (error) {
40
+ res.status(400).json({ error: error instanceof Error ? error.message : "Invalid pagination parameters" });
41
+ }
42
+ });
43
+ governanceRouter.get("/governance/diagnostics", (req, res) => {
44
+ try {
45
+ res.json(memoryEngine.getDiagnostics(scopedUserId(req, req.query.userId)));
46
+ }
47
+ catch (error) {
48
+ res.status(400).json({ error: error instanceof Error ? error.message : "Invalid diagnostics parameters" });
49
+ }
50
+ });
51
+ // GET /api/operations — Timeline feed (all operation types, paginated)
52
+ governanceRouter.get("/operations", (req, res) => {
53
+ try {
54
+ const pagination = PaginationQuerySchema.parse(req.query);
55
+ const userId = scopedUserId(req, req.query.userId);
56
+ const filters = {
57
+ operation: typeof req.query.operation === "string" && req.query.operation !== "all" ? req.query.operation : undefined,
58
+ sessionKey: typeof req.query.sessionKey === "string" && req.query.sessionKey.trim() ? req.query.sessionKey.trim() : undefined,
59
+ createdAfter: typeof req.query.createdAfter === "string" && req.query.createdAfter ? req.query.createdAfter : undefined,
60
+ createdBefore: typeof req.query.createdBefore === "string" && req.query.createdBefore ? req.query.createdBefore : undefined,
61
+ };
62
+ const operations = memoryEngine.getOperationLog(userId, {
63
+ cursor: decodeCursor(pagination.cursor),
64
+ limit: pagination.limit + 1,
65
+ }, filters);
66
+ const page = pageItems(operations, pagination.limit, (op) => ({
67
+ createdAt: op.createdAt,
68
+ id: op.id,
69
+ }));
70
+ res.json({ operations: page.items, nextCursor: page.nextCursor, limit: pagination.limit, hasMore: Boolean(page.nextCursor) });
71
+ }
72
+ catch (error) {
73
+ res.status(400).json({ error: error instanceof Error ? error.message : "Invalid parameters" });
74
+ }
75
+ });
76
+ // POST /api/recall/explain — Recall Inspector
77
+ governanceRouter.post("/recall/explain", async (req, res) => {
78
+ try {
79
+ const { query, sessionKey, activeSkill, userId: requestedUserId } = req.body;
80
+ if (!query || typeof query !== "string" || query.trim().length === 0) {
81
+ res.status(400).json({ error: "query is required" });
82
+ return;
83
+ }
84
+ const result = await memoryEngine.explainRecall({
85
+ userId: scopedUserId(req, requestedUserId),
86
+ sessionKey: sessionKey ?? `inspector_${Date.now()}`,
87
+ query: query.trim(),
88
+ activeSkill,
89
+ });
90
+ res.json(result);
91
+ }
92
+ catch (error) {
93
+ res.status(500).json({ error: error instanceof Error ? error.message : "Explain recall failed" });
94
+ }
95
+ });
@@ -0,0 +1 @@
1
+ export declare const graphRouter: import("express-serve-static-core").Router;