@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.
- package/.env.example +144 -0
- package/README.md +56 -0
- package/agents/README.md +120 -0
- package/agents/code-reviewer.md +97 -0
- package/agents/security-auditor.md +101 -0
- package/agents/test-engineer.md +95 -0
- package/dist/__tests__/agent_mode.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.js +170 -0
- package/dist/__tests__/crypto.test.d.ts +1 -0
- package/dist/__tests__/crypto.test.js +28 -0
- package/dist/__tests__/host-integrations.test.d.ts +1 -0
- package/dist/__tests__/host-integrations.test.js +82 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +50 -0
- package/dist/__tests__/loader.test.d.ts +1 -0
- package/dist/__tests__/loader.test.js +89 -0
- package/dist/__tests__/neural-spark.test.d.ts +1 -0
- package/dist/__tests__/neural-spark.test.js +112 -0
- package/dist/__tests__/pagination.test.d.ts +1 -0
- package/dist/__tests__/pagination.test.js +23 -0
- package/dist/__tests__/redaction.test.d.ts +1 -0
- package/dist/__tests__/redaction.test.js +17 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +56 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +30 -0
- package/dist/__tests__/skill-activation.test.d.ts +1 -0
- package/dist/__tests__/skill-activation.test.js +112 -0
- package/dist/__tests__/working-memory.test.d.ts +1 -0
- package/dist/__tests__/working-memory.test.js +200 -0
- package/dist/__tests__/workspace-paths.test.d.ts +1 -0
- package/dist/__tests__/workspace-paths.test.js +56 -0
- package/dist/__tests__/writer.test.d.ts +1 -0
- package/dist/__tests__/writer.test.js +94 -0
- package/dist/api/auth/crypto.d.ts +4 -0
- package/dist/api/auth/crypto.js +54 -0
- package/dist/api/middleware/auth.d.ts +12 -0
- package/dist/api/middleware/auth.js +90 -0
- package/dist/api/pagination.d.ts +18 -0
- package/dist/api/pagination.js +32 -0
- package/dist/api/routes/auth.d.ts +1 -0
- package/dist/api/routes/auth.js +130 -0
- package/dist/api/routes/chat-completions.d.ts +7 -0
- package/dist/api/routes/chat-completions.js +474 -0
- package/dist/api/routes/contradictions.d.ts +1 -0
- package/dist/api/routes/contradictions.js +28 -0
- package/dist/api/routes/evidence.d.ts +1 -0
- package/dist/api/routes/evidence.js +59 -0
- package/dist/api/routes/governance.d.ts +1 -0
- package/dist/api/routes/governance.js +95 -0
- package/dist/api/routes/graph.d.ts +1 -0
- package/dist/api/routes/graph.js +25 -0
- package/dist/api/routes/hooks.d.ts +1 -0
- package/dist/api/routes/hooks.js +88 -0
- package/dist/api/routes/memories.d.ts +1 -0
- package/dist/api/routes/memories.js +92 -0
- package/dist/api/routes/persona.d.ts +1 -0
- package/dist/api/routes/persona.js +9 -0
- package/dist/api/routes/scenes.d.ts +1 -0
- package/dist/api/routes/scenes.js +35 -0
- package/dist/api/routes/skills.d.ts +1 -0
- package/dist/api/routes/skills.js +14 -0
- package/dist/api/routes/stats.d.ts +1 -0
- package/dist/api/routes/stats.js +8 -0
- package/dist/api/routes/users.d.ts +1 -0
- package/dist/api/routes/users.js +82 -0
- package/dist/api/routes/working.d.ts +1 -0
- package/dist/api/routes/working.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +492 -0
- package/dist/integrations/claude-code.d.ts +12 -0
- package/dist/integrations/claude-code.js +35 -0
- package/dist/integrations/codex.d.ts +12 -0
- package/dist/integrations/codex.js +34 -0
- package/dist/integrations/generic-mcp.d.ts +52 -0
- package/dist/integrations/generic-mcp.js +118 -0
- package/dist/loader.d.ts +29 -0
- package/dist/loader.js +200 -0
- package/dist/memory/capture.d.ts +35 -0
- package/dist/memory/capture.js +230 -0
- package/dist/memory/config.d.ts +2 -0
- package/dist/memory/config.js +3 -0
- package/dist/memory/engine.d.ts +203 -0
- package/dist/memory/engine.js +626 -0
- package/dist/memory/llm-semaphore.d.ts +41 -0
- package/dist/memory/llm-semaphore.js +81 -0
- package/dist/memory/memory-type-config.d.ts +11 -0
- package/dist/memory/memory-type-config.js +65 -0
- package/dist/memory/pipeline/cognitive-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/cognitive-contradiction.js +59 -0
- package/dist/memory/pipeline/cognitive-dedup.d.ts +23 -0
- package/dist/memory/pipeline/cognitive-dedup.js +38 -0
- package/dist/memory/pipeline/cognitive-extractor.d.ts +21 -0
- package/dist/memory/pipeline/cognitive-extractor.js +183 -0
- package/dist/memory/pipeline/contextual-focus-builder.d.ts +13 -0
- package/dist/memory/pipeline/contextual-focus-builder.js +135 -0
- package/dist/memory/pipeline/focus-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/focus-direction-shift.js +27 -0
- package/dist/memory/pipeline/graph-builder.d.ts +11 -0
- package/dist/memory/pipeline/graph-builder.js +88 -0
- package/dist/memory/pipeline/graph-recall.d.ts +13 -0
- package/dist/memory/pipeline/graph-recall.js +55 -0
- package/dist/memory/pipeline/identity-distiller.d.ts +15 -0
- package/dist/memory/pipeline/identity-distiller.js +40 -0
- package/dist/memory/pipeline/l1-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/l1-contradiction.js +66 -0
- package/dist/memory/pipeline/l1-dedup.d.ts +23 -0
- package/dist/memory/pipeline/l1-dedup.js +39 -0
- package/dist/memory/pipeline/l1-extractor.d.ts +21 -0
- package/dist/memory/pipeline/l1-extractor.js +180 -0
- package/dist/memory/pipeline/l2-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/l2-direction-shift.js +27 -0
- package/dist/memory/pipeline/l2-scene.d.ts +15 -0
- package/dist/memory/pipeline/l2-scene.js +140 -0
- package/dist/memory/pipeline/l3-distiller.d.ts +15 -0
- package/dist/memory/pipeline/l3-distiller.js +40 -0
- package/dist/memory/pipeline/neural-spark.d.ts +27 -0
- package/dist/memory/pipeline/neural-spark.js +78 -0
- package/dist/memory/pipeline/skill-prewarm.d.ts +63 -0
- package/dist/memory/pipeline/skill-prewarm.js +127 -0
- package/dist/memory/pipeline/task-queue.d.ts +54 -0
- package/dist/memory/pipeline/task-queue.js +117 -0
- package/dist/memory/prompts/cognitive-contradiction.d.ts +1 -0
- package/dist/memory/prompts/cognitive-contradiction.js +25 -0
- package/dist/memory/prompts/cognitive-extraction.d.ts +10 -0
- package/dist/memory/prompts/cognitive-extraction.js +114 -0
- package/dist/memory/prompts/core-identity.d.ts +6 -0
- package/dist/memory/prompts/core-identity.js +60 -0
- package/dist/memory/prompts/focus-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/focus-direction-shift.js +32 -0
- package/dist/memory/prompts/focus-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/focus-scene-cluster.js +33 -0
- package/dist/memory/prompts/focus-scene.d.ts +7 -0
- package/dist/memory/prompts/focus-scene.js +40 -0
- package/dist/memory/prompts/graph-extraction-batch.d.ts +14 -0
- package/dist/memory/prompts/graph-extraction-batch.js +54 -0
- package/dist/memory/prompts/graph-extraction.d.ts +2 -0
- package/dist/memory/prompts/graph-extraction.js +53 -0
- package/dist/memory/prompts/l1-contradiction-batch.d.ts +16 -0
- package/dist/memory/prompts/l1-contradiction-batch.js +47 -0
- package/dist/memory/prompts/l1-contradiction.d.ts +1 -0
- package/dist/memory/prompts/l1-contradiction.js +25 -0
- package/dist/memory/prompts/l1-extraction.d.ts +10 -0
- package/dist/memory/prompts/l1-extraction.js +114 -0
- package/dist/memory/prompts/l2-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/l2-direction-shift.js +32 -0
- package/dist/memory/prompts/l2-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/l2-scene-cluster.js +33 -0
- package/dist/memory/prompts/l2-scene.d.ts +7 -0
- package/dist/memory/prompts/l2-scene.js +40 -0
- package/dist/memory/prompts/l3-persona.d.ts +6 -0
- package/dist/memory/prompts/l3-persona.js +60 -0
- package/dist/memory/recall.d.ts +47 -0
- package/dist/memory/recall.js +427 -0
- package/dist/memory/redaction.d.ts +1 -0
- package/dist/memory/redaction.js +24 -0
- package/dist/memory/retry.d.ts +13 -0
- package/dist/memory/retry.js +53 -0
- package/dist/memory/scheduler.d.ts +9 -0
- package/dist/memory/scheduler.js +16 -0
- package/dist/memory/skill-hints-loader.d.ts +30 -0
- package/dist/memory/skill-hints-loader.js +100 -0
- package/dist/memory/store/embedding.d.ts +16 -0
- package/dist/memory/store/embedding.js +68 -0
- package/dist/memory/store/reranker.d.ts +24 -0
- package/dist/memory/store/reranker.js +83 -0
- package/dist/memory/store/sqlite.d.ts +167 -0
- package/dist/memory/store/sqlite.js +1816 -0
- package/dist/memory/store/types.d.ts +101 -0
- package/dist/memory/store/types.js +1 -0
- package/dist/memory/types.d.ts +207 -0
- package/dist/memory/types.js +7 -0
- package/dist/memory/validation.d.ts +441 -0
- package/dist/memory/validation.js +129 -0
- package/dist/memory/working/canvas.d.ts +5 -0
- package/dist/memory/working/canvas.js +43 -0
- package/dist/memory/working/offload.d.ts +71 -0
- package/dist/memory/working/offload.js +211 -0
- package/dist/memory/working/step-log.d.ts +16 -0
- package/dist/memory/working/step-log.js +35 -0
- package/dist/registry.d.ts +34 -0
- package/dist/registry.js +305 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.js +126 -0
- package/dist/scripts/validate-foreign-workspace-path.d.ts +1 -0
- package/dist/scripts/validate-foreign-workspace-path.js +39 -0
- package/dist/tools/agent_memory_tools.d.ts +485 -0
- package/dist/tools/agent_memory_tools.js +793 -0
- package/dist/tools/create_skill.d.ts +46 -0
- package/dist/tools/create_skill.js +46 -0
- package/dist/tools/get_doc.d.ts +21 -0
- package/dist/tools/get_doc.js +24 -0
- package/dist/tools/get_persona.d.ts +15 -0
- package/dist/tools/get_persona.js +20 -0
- package/dist/tools/get_reference.d.ts +15 -0
- package/dist/tools/get_reference.js +20 -0
- package/dist/tools/get_skill.d.ts +34 -0
- package/dist/tools/get_skill.js +65 -0
- package/dist/tools/get_template_doc.d.ts +21 -0
- package/dist/tools/get_template_doc.js +24 -0
- package/dist/tools/list_docs.d.ts +15 -0
- package/dist/tools/list_docs.js +16 -0
- package/dist/tools/list_skills.d.ts +18 -0
- package/dist/tools/list_skills.js +17 -0
- package/dist/tools/list_template_docs.d.ts +15 -0
- package/dist/tools/list_template_docs.js +16 -0
- package/dist/tools/memory-engineering.d.ts +225 -0
- package/dist/tools/memory-engineering.js +284 -0
- package/dist/tools/memory-explain.d.ts +34 -0
- package/dist/tools/memory-explain.js +109 -0
- package/dist/tools/memory-governance.d.ts +171 -0
- package/dist/tools/memory-governance.js +224 -0
- package/dist/tools/memory-hooks.d.ts +67 -0
- package/dist/tools/memory-hooks.js +102 -0
- package/dist/tools/memory-working.d.ts +98 -0
- package/dist/tools/memory-working.js +101 -0
- package/dist/tools/memory_capture_turn.d.ts +66 -0
- package/dist/tools/memory_capture_turn.js +85 -0
- package/dist/tools/memory_consolidate.d.ts +55 -0
- package/dist/tools/memory_consolidate.js +176 -0
- package/dist/tools/memory_contradictions.d.ts +53 -0
- package/dist/tools/memory_contradictions.js +52 -0
- package/dist/tools/memory_graph_query.d.ts +51 -0
- package/dist/tools/memory_graph_query.js +35 -0
- package/dist/tools/memory_mark_cited.d.ts +43 -0
- package/dist/tools/memory_mark_cited.js +63 -0
- package/dist/tools/memory_recall.d.ts +77 -0
- package/dist/tools/memory_recall.js +81 -0
- package/dist/tools/memory_register_skill_hints.d.ts +49 -0
- package/dist/tools/memory_register_skill_hints.js +55 -0
- package/dist/tools/memory_resolve_session.d.ts +24 -0
- package/dist/tools/memory_resolve_session.js +133 -0
- package/dist/tools/memory_search.d.ts +146 -0
- package/dist/tools/memory_search.js +84 -0
- package/dist/tools/search_skills.d.ts +18 -0
- package/dist/tools/search_skills.js +17 -0
- package/dist/tools/update_doc.d.ts +24 -0
- package/dist/tools/update_doc.js +35 -0
- package/dist/tools/update_skill.d.ts +30 -0
- package/dist/tools/update_skill.js +80 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +4 -0
- package/dist/writer.d.ts +30 -0
- package/dist/writer.js +220 -0
- package/docs/TEMPLATE ONLY +1 -0
- package/docs/api/API.md +64 -0
- package/docs/api/security/SECURITY.md +58 -0
- package/docs/deployment/DockerDeployment.md +30 -0
- package/docs/design/Design.md +59 -0
- package/docs/design/themes/apple.md +101 -0
- package/docs/design/themes/dieter-grid.md +100 -0
- package/docs/design/themes/gallery-white.md +100 -0
- package/docs/design/themes/pinterest.md +101 -0
- package/docs/design/themes/realty-open-house.md +101 -0
- package/docs/design/themes/vodafone.md +101 -0
- package/docs/hooks/Hooks.md +30 -0
- package/docs/schema/Schema.md +35 -0
- package/docs/strategy/ScalingStrategy.md +19 -0
- package/package.json +88 -0
- package/references/accessibility-checklist.md +160 -0
- package/references/orchestration-patterns.md +370 -0
- package/references/performance-checklist.md +153 -0
- package/references/security-checklist.md +134 -0
- package/references/testing-patterns.md +236 -0
- package/skills/agent/adr-skill/SKILL.md +299 -0
- package/skills/agent/agentic-engineering-workflow/SKILL.md +95 -0
- package/skills/agent/bootstrap-skill/SKILL.md +103 -0
- package/skills/agent/context-engineering/SKILL.md +307 -0
- package/skills/agent/debugging-and-error-recovery/SKILL.md +308 -0
- package/skills/agent/developer-growth-analysis/SKILL.md +328 -0
- package/skills/agent/doubt-driven-skill/SKILL.md +249 -0
- package/skills/agent/handover-skill/SKILL.md +112 -0
- package/skills/agent/idea-refine-skill/SKILL.md +185 -0
- package/skills/agent/idea-refine-skill/examples.md +238 -0
- package/skills/agent/idea-refine-skill/frameworks.md +99 -0
- package/skills/agent/idea-refine-skill/refinement-criteria.md +113 -0
- package/skills/agent/interview-skill/SKILL.md +226 -0
- package/skills/agent/planning-skill/SKILL.md +270 -0
- package/skills/agent/skill-authoring/SKILL.md +189 -0
- package/skills/agent/source-driven-skill/SKILL.md +197 -0
- package/skills/agent/spec-driven-skill/SKILL.md +221 -0
- package/skills/agent/sync-skill/SKILL.md +92 -0
- package/skills/agent/using-agent-skills/SKILL.md +189 -0
- package/skills/api/a11y-skill/SKILL.md +88 -0
- package/skills/api/api-skill/SKILL.md +123 -0
- package/skills/api/auth-skill/SKILL.md +80 -0
- package/skills/api/debug-skill/SKILL.md +535 -0
- package/skills/api/performance-skill/SKILL.md +100 -0
- package/skills/api/testing-skill/SKILL.md +100 -0
- package/skills/codebase/code-review-and-quality/SKILL.md +228 -0
- package/skills/codebase/code-simplification/SKILL.md +352 -0
- package/skills/codebase/code-structure-cleanup/SKILL.md +142 -0
- package/skills/codebase/concerns-skill/SKILL.md +89 -0
- package/skills/codebase/conventions-skill/SKILL.md +95 -0
- package/skills/codebase/doc-management-skill/SKILL.md +47 -0
- package/skills/codebase/git-workflow-skill/SKILL.md +312 -0
- package/skills/communication/1-3-1-rule/SKILL.md +120 -0
- package/skills/design/brutalist-skill/SKILL.md +131 -0
- package/skills/design/concept-diagrams/SKILL.md +387 -0
- package/skills/design/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
- package/skills/design/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
- package/skills/design/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
- package/skills/design/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
- package/skills/design/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
- package/skills/design/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
- package/skills/design/concept-diagrams/examples/electricity-grid-flow.md +182 -0
- package/skills/design/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
- package/skills/design/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
- package/skills/design/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
- package/skills/design/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
- package/skills/design/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
- package/skills/design/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
- package/skills/design/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
- package/skills/design/concept-diagrams/examples/wind-turbine-structure.md +338 -0
- package/skills/design/concept-diagrams/references/dashboard-patterns.md +43 -0
- package/skills/design/concept-diagrams/references/infrastructure-patterns.md +144 -0
- package/skills/design/concept-diagrams/references/physical-shape-cookbook.md +42 -0
- package/skills/design/concept-diagrams/templates/template.html +174 -0
- package/skills/design/gpt-tasteskill/SKILL.md +114 -0
- package/skills/design/minimalist-skill/SKILL.md +116 -0
- package/skills/design/output-skill/SKILL.md +87 -0
- package/skills/design/redesign-skill/SKILL.md +213 -0
- package/skills/design/soft-skill/SKILL.md +132 -0
- package/skills/design/stitch-skill/EXAMPLE.md +121 -0
- package/skills/design/stitch-skill/SKILL.md +222 -0
- package/skills/design/taste-skill/SKILL.md +269 -0
- package/skills/devops/ci-cd-skill/SKILL.md +402 -0
- package/skills/devops/docker-skill/SKILL.md +297 -0
- package/skills/devops/domain-skill/SKILL.md +234 -0
- package/skills/lifecycle/changelog-generator/SKILL.md +135 -0
- package/skills/lifecycle/incremental-skill/SKILL.md +257 -0
- package/skills/lifecycle/migration-skill/SKILL.md +218 -0
- package/skills/lifecycle/shipping-skill/SKILL.md +321 -0
- package/skills/memory/agent-memory/SKILL.md +122 -0
- package/skills/qa/browser-testing-skill/SKILL.md +314 -0
- 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;
|