shieldcortex 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +282 -0
- package/dashboard/components.json +22 -0
- package/dashboard/eslint.config.mjs +42 -0
- package/dashboard/next.config.ts +7 -0
- package/dashboard/package-lock.json +8053 -0
- package/dashboard/package.json +44 -0
- package/dashboard/postcss.config.mjs +7 -0
- package/dashboard/public/file.svg +1 -0
- package/dashboard/public/globe.svg +1 -0
- package/dashboard/public/next.svg +1 -0
- package/dashboard/public/vercel.svg +1 -0
- package/dashboard/public/window.svg +1 -0
- package/dashboard/scripts/ensure-api.mjs +76 -0
- package/dashboard/src/app/error.tsx +49 -0
- package/dashboard/src/app/favicon.ico +0 -0
- package/dashboard/src/app/globals.css +130 -0
- package/dashboard/src/app/layout.tsx +35 -0
- package/dashboard/src/app/page.tsx +364 -0
- package/dashboard/src/components/Providers.tsx +27 -0
- package/dashboard/src/components/brain/ActivityPulseSystem.tsx +229 -0
- package/dashboard/src/components/brain/BrainMesh.tsx +133 -0
- package/dashboard/src/components/brain/BrainRegions.tsx +254 -0
- package/dashboard/src/components/brain/BrainScene.tsx +255 -0
- package/dashboard/src/components/brain/CategoryLabels.tsx +103 -0
- package/dashboard/src/components/brain/CoreSphere.tsx +215 -0
- package/dashboard/src/components/brain/DataFlowParticles.tsx +123 -0
- package/dashboard/src/components/brain/DataStreamRings.tsx +161 -0
- package/dashboard/src/components/brain/ElectronFlow.tsx +323 -0
- package/dashboard/src/components/brain/HolographicGrid.tsx +235 -0
- package/dashboard/src/components/brain/MemoryLinks.tsx +271 -0
- package/dashboard/src/components/brain/MemoryNode.tsx +245 -0
- package/dashboard/src/components/brain/NeuralPathways.tsx +441 -0
- package/dashboard/src/components/brain/SynapseNodes.tsx +312 -0
- package/dashboard/src/components/brain/TimelineControls.tsx +205 -0
- package/dashboard/src/components/chip/ChipScene.tsx +497 -0
- package/dashboard/src/components/chip/ChipSubstrate.tsx +238 -0
- package/dashboard/src/components/chip/CortexCore.tsx +210 -0
- package/dashboard/src/components/chip/DataBus.tsx +416 -0
- package/dashboard/src/components/chip/MemoryCell.tsx +225 -0
- package/dashboard/src/components/chip/MemoryGrid.tsx +328 -0
- package/dashboard/src/components/chip/QuantumCell.tsx +316 -0
- package/dashboard/src/components/chip/SectionLabel.tsx +113 -0
- package/dashboard/src/components/chip/index.ts +14 -0
- package/dashboard/src/components/controls/ControlPanel.tsx +106 -0
- package/dashboard/src/components/controls/VersionPanel.tsx +185 -0
- package/dashboard/src/components/dashboard/StatsPanel.tsx +164 -0
- package/dashboard/src/components/debug/ActivityLog.tsx +250 -0
- package/dashboard/src/components/debug/DebugPanel.tsx +101 -0
- package/dashboard/src/components/debug/QueryTester.tsx +192 -0
- package/dashboard/src/components/debug/RelationshipGraph.tsx +403 -0
- package/dashboard/src/components/debug/SqlConsole.tsx +319 -0
- package/dashboard/src/components/graph/KnowledgeGraph.tsx +230 -0
- package/dashboard/src/components/graph/OntologyGraph.tsx +631 -0
- package/dashboard/src/components/insights/ActivityHeatmap.tsx +131 -0
- package/dashboard/src/components/insights/InsightsView.tsx +46 -0
- package/dashboard/src/components/insights/KnowledgeMapPanel.tsx +80 -0
- package/dashboard/src/components/insights/QualityPanel.tsx +116 -0
- package/dashboard/src/components/memories/MemoriesView.tsx +150 -0
- package/dashboard/src/components/memories/MemoryCard.tsx +103 -0
- package/dashboard/src/components/memory/MemoryDetail.tsx +325 -0
- package/dashboard/src/components/nav/NavRail.tsx +54 -0
- package/dashboard/src/components/ui/button.tsx +62 -0
- package/dashboard/src/components/ui/card.tsx +92 -0
- package/dashboard/src/components/ui/input.tsx +21 -0
- package/dashboard/src/hooks/useDebouncedValue.ts +24 -0
- package/dashboard/src/hooks/useMemories.ts +458 -0
- package/dashboard/src/hooks/useSuggestions.ts +46 -0
- package/dashboard/src/lib/category-colors.ts +84 -0
- package/dashboard/src/lib/position-algorithm.ts +177 -0
- package/dashboard/src/lib/simplex-noise.ts +217 -0
- package/dashboard/src/lib/store.ts +88 -0
- package/dashboard/src/lib/utils.ts +6 -0
- package/dashboard/src/lib/websocket.ts +249 -0
- package/dashboard/src/types/memory.ts +73 -0
- package/dashboard/tsconfig.json +34 -0
- package/dist/__tests__/consolidation-merge.test.d.ts +9 -0
- package/dist/__tests__/consolidation-merge.test.d.ts.map +1 -0
- package/dist/__tests__/consolidation-merge.test.js +137 -0
- package/dist/__tests__/consolidation-merge.test.js.map +1 -0
- package/dist/__tests__/contradictions.test.d.ts +8 -0
- package/dist/__tests__/contradictions.test.d.ts.map +1 -0
- package/dist/__tests__/contradictions.test.js +78 -0
- package/dist/__tests__/contradictions.test.js.map +1 -0
- package/dist/__tests__/salience-evolution.test.d.ts +7 -0
- package/dist/__tests__/salience-evolution.test.d.ts.map +1 -0
- package/dist/__tests__/salience-evolution.test.js +151 -0
- package/dist/__tests__/salience-evolution.test.js.map +1 -0
- package/dist/__tests__/store.test.d.ts +7 -0
- package/dist/__tests__/store.test.d.ts.map +1 -0
- package/dist/__tests__/store.test.js +582 -0
- package/dist/__tests__/store.test.js.map +1 -0
- package/dist/api/control.d.ts +27 -0
- package/dist/api/control.d.ts.map +1 -0
- package/dist/api/control.js +60 -0
- package/dist/api/control.js.map +1 -0
- package/dist/api/events.d.ts +159 -0
- package/dist/api/events.d.ts.map +1 -0
- package/dist/api/events.js +155 -0
- package/dist/api/events.js.map +1 -0
- package/dist/api/version.d.ts +36 -0
- package/dist/api/version.d.ts.map +1 -0
- package/dist/api/version.js +146 -0
- package/dist/api/version.js.map +1 -0
- package/dist/api/visualization-server.d.ts +11 -0
- package/dist/api/visualization-server.d.ts.map +1 -0
- package/dist/api/visualization-server.js +1186 -0
- package/dist/api/visualization-server.js.map +1 -0
- package/dist/context/project-context.d.ts +57 -0
- package/dist/context/project-context.d.ts.map +1 -0
- package/dist/context/project-context.js +135 -0
- package/dist/context/project-context.js.map +1 -0
- package/dist/database/init.d.ts +49 -0
- package/dist/database/init.d.ts.map +1 -0
- package/dist/database/init.js +567 -0
- package/dist/database/init.js.map +1 -0
- package/dist/defence/__tests__/firewall.test.d.ts +8 -0
- package/dist/defence/__tests__/firewall.test.d.ts.map +1 -0
- package/dist/defence/__tests__/firewall.test.js +123 -0
- package/dist/defence/__tests__/firewall.test.js.map +1 -0
- package/dist/defence/__tests__/fragmentation.test.d.ts +7 -0
- package/dist/defence/__tests__/fragmentation.test.d.ts.map +1 -0
- package/dist/defence/__tests__/fragmentation.test.js +51 -0
- package/dist/defence/__tests__/fragmentation.test.js.map +1 -0
- package/dist/defence/__tests__/pipeline.test.d.ts +8 -0
- package/dist/defence/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/defence/__tests__/pipeline.test.js +61 -0
- package/dist/defence/__tests__/pipeline.test.js.map +1 -0
- package/dist/defence/__tests__/sensitivity.test.d.ts +7 -0
- package/dist/defence/__tests__/sensitivity.test.d.ts.map +1 -0
- package/dist/defence/__tests__/sensitivity.test.js +61 -0
- package/dist/defence/__tests__/sensitivity.test.js.map +1 -0
- package/dist/defence/__tests__/trust.test.d.ts +7 -0
- package/dist/defence/__tests__/trust.test.d.ts.map +1 -0
- package/dist/defence/__tests__/trust.test.js +49 -0
- package/dist/defence/__tests__/trust.test.js.map +1 -0
- package/dist/defence/audit/index.d.ts +4 -0
- package/dist/defence/audit/index.d.ts.map +1 -0
- package/dist/defence/audit/index.js +3 -0
- package/dist/defence/audit/index.js.map +1 -0
- package/dist/defence/audit/logger.d.ts +14 -0
- package/dist/defence/audit/logger.d.ts.map +1 -0
- package/dist/defence/audit/logger.js +54 -0
- package/dist/defence/audit/logger.js.map +1 -0
- package/dist/defence/audit/queries.d.ts +33 -0
- package/dist/defence/audit/queries.d.ts.map +1 -0
- package/dist/defence/audit/queries.js +103 -0
- package/dist/defence/audit/queries.js.map +1 -0
- package/dist/defence/firewall/anomaly-scorer.d.ts +8 -0
- package/dist/defence/firewall/anomaly-scorer.d.ts.map +1 -0
- package/dist/defence/firewall/anomaly-scorer.js +58 -0
- package/dist/defence/firewall/anomaly-scorer.js.map +1 -0
- package/dist/defence/firewall/encoding-detector.d.ts +13 -0
- package/dist/defence/firewall/encoding-detector.d.ts.map +1 -0
- package/dist/defence/firewall/encoding-detector.js +120 -0
- package/dist/defence/firewall/encoding-detector.js.map +1 -0
- package/dist/defence/firewall/index.d.ts +21 -0
- package/dist/defence/firewall/index.d.ts.map +1 -0
- package/dist/defence/firewall/index.js +133 -0
- package/dist/defence/firewall/index.js.map +1 -0
- package/dist/defence/firewall/instruction-detector.d.ts +12 -0
- package/dist/defence/firewall/instruction-detector.d.ts.map +1 -0
- package/dist/defence/firewall/instruction-detector.js +99 -0
- package/dist/defence/firewall/instruction-detector.js.map +1 -0
- package/dist/defence/firewall/privilege-detector.d.ts +13 -0
- package/dist/defence/firewall/privilege-detector.d.ts.map +1 -0
- package/dist/defence/firewall/privilege-detector.js +89 -0
- package/dist/defence/firewall/privilege-detector.js.map +1 -0
- package/dist/defence/fragmentation/assembly-detector.d.ts +18 -0
- package/dist/defence/fragmentation/assembly-detector.d.ts.map +1 -0
- package/dist/defence/fragmentation/assembly-detector.js +72 -0
- package/dist/defence/fragmentation/assembly-detector.js.map +1 -0
- package/dist/defence/fragmentation/entity-extractor.d.ts +19 -0
- package/dist/defence/fragmentation/entity-extractor.d.ts.map +1 -0
- package/dist/defence/fragmentation/entity-extractor.js +86 -0
- package/dist/defence/fragmentation/entity-extractor.js.map +1 -0
- package/dist/defence/fragmentation/index.d.ts +23 -0
- package/dist/defence/fragmentation/index.d.ts.map +1 -0
- package/dist/defence/fragmentation/index.js +49 -0
- package/dist/defence/fragmentation/index.js.map +1 -0
- package/dist/defence/fragmentation/temporal-analyzer.d.ts +28 -0
- package/dist/defence/fragmentation/temporal-analyzer.d.ts.map +1 -0
- package/dist/defence/fragmentation/temporal-analyzer.js +41 -0
- package/dist/defence/fragmentation/temporal-analyzer.js.map +1 -0
- package/dist/defence/index.d.ts +12 -0
- package/dist/defence/index.d.ts.map +1 -0
- package/dist/defence/index.js +18 -0
- package/dist/defence/index.js.map +1 -0
- package/dist/defence/pipeline.d.ts +9 -0
- package/dist/defence/pipeline.d.ts.map +1 -0
- package/dist/defence/pipeline.js +115 -0
- package/dist/defence/pipeline.js.map +1 -0
- package/dist/defence/scanner/index.d.ts +5 -0
- package/dist/defence/scanner/index.d.ts.map +1 -0
- package/dist/defence/scanner/index.js +5 -0
- package/dist/defence/scanner/index.js.map +1 -0
- package/dist/defence/scanner/scan-existing.d.ts +34 -0
- package/dist/defence/scanner/scan-existing.d.ts.map +1 -0
- package/dist/defence/scanner/scan-existing.js +136 -0
- package/dist/defence/scanner/scan-existing.js.map +1 -0
- package/dist/defence/sensitivity/classifier.d.ts +6 -0
- package/dist/defence/sensitivity/classifier.d.ts.map +1 -0
- package/dist/defence/sensitivity/classifier.js +50 -0
- package/dist/defence/sensitivity/classifier.js.map +1 -0
- package/dist/defence/sensitivity/index.d.ts +11 -0
- package/dist/defence/sensitivity/index.d.ts.map +1 -0
- package/dist/defence/sensitivity/index.js +13 -0
- package/dist/defence/sensitivity/index.js.map +1 -0
- package/dist/defence/sensitivity/patterns.d.ts +14 -0
- package/dist/defence/sensitivity/patterns.d.ts.map +1 -0
- package/dist/defence/sensitivity/patterns.js +67 -0
- package/dist/defence/sensitivity/patterns.js.map +1 -0
- package/dist/defence/sensitivity/redaction.d.ts +17 -0
- package/dist/defence/sensitivity/redaction.d.ts.map +1 -0
- package/dist/defence/sensitivity/redaction.js +47 -0
- package/dist/defence/sensitivity/redaction.js.map +1 -0
- package/dist/defence/trust/index.d.ts +3 -0
- package/dist/defence/trust/index.d.ts.map +1 -0
- package/dist/defence/trust/index.js +3 -0
- package/dist/defence/trust/index.js.map +1 -0
- package/dist/defence/trust/recall-filter.d.ts +10 -0
- package/dist/defence/trust/recall-filter.d.ts.map +1 -0
- package/dist/defence/trust/recall-filter.js +38 -0
- package/dist/defence/trust/recall-filter.js.map +1 -0
- package/dist/defence/trust/source-scorer.d.ts +6 -0
- package/dist/defence/trust/source-scorer.d.ts.map +1 -0
- package/dist/defence/trust/source-scorer.js +34 -0
- package/dist/defence/trust/source-scorer.js.map +1 -0
- package/dist/defence/types.d.ts +88 -0
- package/dist/defence/types.d.ts.map +1 -0
- package/dist/defence/types.js +15 -0
- package/dist/defence/types.js.map +1 -0
- package/dist/embeddings/generator.d.ts +20 -0
- package/dist/embeddings/generator.d.ts.map +1 -0
- package/dist/embeddings/generator.js +83 -0
- package/dist/embeddings/generator.js.map +1 -0
- package/dist/embeddings/index.d.ts +2 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +2 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +131 -0
- package/dist/errors.js.map +1 -0
- package/dist/graph/backfill.d.ts +6 -0
- package/dist/graph/backfill.d.ts.map +1 -0
- package/dist/graph/backfill.js +33 -0
- package/dist/graph/backfill.js.map +1 -0
- package/dist/graph/extract.d.ts +21 -0
- package/dist/graph/extract.d.ts.map +1 -0
- package/dist/graph/extract.js +231 -0
- package/dist/graph/extract.js.map +1 -0
- package/dist/graph/resolve.d.ts +6 -0
- package/dist/graph/resolve.d.ts.map +1 -0
- package/dist/graph/resolve.js +126 -0
- package/dist/graph/resolve.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +248 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/activation.d.ts +69 -0
- package/dist/memory/activation.d.ts.map +1 -0
- package/dist/memory/activation.js +168 -0
- package/dist/memory/activation.js.map +1 -0
- package/dist/memory/consolidate.d.ts +98 -0
- package/dist/memory/consolidate.d.ts.map +1 -0
- package/dist/memory/consolidate.js +511 -0
- package/dist/memory/consolidate.js.map +1 -0
- package/dist/memory/contradiction.d.ts +69 -0
- package/dist/memory/contradiction.d.ts.map +1 -0
- package/dist/memory/contradiction.js +286 -0
- package/dist/memory/contradiction.js.map +1 -0
- package/dist/memory/decay.d.ts +62 -0
- package/dist/memory/decay.d.ts.map +1 -0
- package/dist/memory/decay.js +184 -0
- package/dist/memory/decay.js.map +1 -0
- package/dist/memory/salience.d.ts +36 -0
- package/dist/memory/salience.d.ts.map +1 -0
- package/dist/memory/salience.js +216 -0
- package/dist/memory/salience.js.map +1 -0
- package/dist/memory/similarity.d.ts +57 -0
- package/dist/memory/similarity.d.ts.map +1 -0
- package/dist/memory/similarity.js +114 -0
- package/dist/memory/similarity.js.map +1 -0
- package/dist/memory/store.d.ts +179 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +1184 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/types.d.ts +97 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +30 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +568 -0
- package/dist/server.js.map +1 -0
- package/dist/service/install.d.ts +15 -0
- package/dist/service/install.d.ts.map +1 -0
- package/dist/service/install.js +178 -0
- package/dist/service/install.js.map +1 -0
- package/dist/service/templates.d.ts +13 -0
- package/dist/service/templates.d.ts.map +1 -0
- package/dist/service/templates.js +58 -0
- package/dist/service/templates.js.map +1 -0
- package/dist/setup/claude-md.d.ts +12 -0
- package/dist/setup/claude-md.d.ts.map +1 -0
- package/dist/setup/claude-md.js +68 -0
- package/dist/setup/claude-md.js.map +1 -0
- package/dist/setup/clawdbot.d.ts +15 -0
- package/dist/setup/clawdbot.d.ts.map +1 -0
- package/dist/setup/clawdbot.js +118 -0
- package/dist/setup/clawdbot.js.map +1 -0
- package/dist/setup/doctor.d.ts +5 -0
- package/dist/setup/doctor.d.ts.map +1 -0
- package/dist/setup/doctor.js +141 -0
- package/dist/setup/doctor.js.map +1 -0
- package/dist/setup/hooks.d.ts +6 -0
- package/dist/setup/hooks.d.ts.map +1 -0
- package/dist/setup/hooks.js +36 -0
- package/dist/setup/hooks.js.map +1 -0
- package/dist/setup/migrate.d.ts +16 -0
- package/dist/setup/migrate.d.ts.map +1 -0
- package/dist/setup/migrate.js +164 -0
- package/dist/setup/migrate.js.map +1 -0
- package/dist/setup/settings-hooks.d.ts +7 -0
- package/dist/setup/settings-hooks.d.ts.map +1 -0
- package/dist/setup/settings-hooks.js +83 -0
- package/dist/setup/settings-hooks.js.map +1 -0
- package/dist/setup/uninstall.d.ts +12 -0
- package/dist/setup/uninstall.d.ts.map +1 -0
- package/dist/setup/uninstall.js +125 -0
- package/dist/setup/uninstall.js.map +1 -0
- package/dist/tools/context.d.ts +135 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +273 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/forget.d.ts +53 -0
- package/dist/tools/forget.d.ts.map +1 -0
- package/dist/tools/forget.js +179 -0
- package/dist/tools/forget.js.map +1 -0
- package/dist/tools/graph.d.ts +46 -0
- package/dist/tools/graph.d.ts.map +1 -0
- package/dist/tools/graph.js +206 -0
- package/dist/tools/graph.js.map +1 -0
- package/dist/tools/recall.d.ts +79 -0
- package/dist/tools/recall.d.ts.map +1 -0
- package/dist/tools/recall.js +156 -0
- package/dist/tools/recall.js.map +1 -0
- package/dist/tools/remember.d.ts +83 -0
- package/dist/tools/remember.d.ts.map +1 -0
- package/dist/tools/remember.js +151 -0
- package/dist/tools/remember.js.map +1 -0
- package/dist/worker/brain-worker.d.ts +100 -0
- package/dist/worker/brain-worker.d.ts.map +1 -0
- package/dist/worker/brain-worker.js +283 -0
- package/dist/worker/brain-worker.js.map +1 -0
- package/dist/worker/link-discovery.d.ts +47 -0
- package/dist/worker/link-discovery.d.ts.map +1 -0
- package/dist/worker/link-discovery.js +103 -0
- package/dist/worker/link-discovery.js.map +1 -0
- package/dist/worker/predictive-consolidation.d.ts +46 -0
- package/dist/worker/predictive-consolidation.d.ts.map +1 -0
- package/dist/worker/predictive-consolidation.js +110 -0
- package/dist/worker/predictive-consolidation.js.map +1 -0
- package/dist/worker/types.d.ts +91 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +22 -0
- package/dist/worker/types.js.map +1 -0
- package/hooks/clawdbot/cortex-memory/HOOK.md +71 -0
- package/hooks/clawdbot/cortex-memory/handler.js +279 -0
- package/package.json +73 -0
- package/scripts/pre-compact-hook.mjs +716 -0
- package/scripts/session-end-hook.mjs +548 -0
- package/scripts/session-start-hook.mjs +221 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/stop-dashboard.sh +21 -0
- package/scripts/stop-hook.mjs +163 -0
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visualization API Server
|
|
3
|
+
*
|
|
4
|
+
* Provides REST endpoints and WebSocket for the Brain Dashboard.
|
|
5
|
+
* Runs alongside or instead of the MCP server.
|
|
6
|
+
*/
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import cors from 'cors';
|
|
9
|
+
import { createServer } from 'http';
|
|
10
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
11
|
+
import { getDatabase, initDatabase, checkpointWal } from '../database/init.js';
|
|
12
|
+
import { DEFAULT_CONFIG } from '../memory/types.js';
|
|
13
|
+
import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateDecayScores, rowToMemory, } from '../memory/store.js';
|
|
14
|
+
import { consolidate, generateContextSummary, formatContextSummary, } from '../memory/consolidate.js';
|
|
15
|
+
import { calculateDecayedScore } from '../memory/decay.js';
|
|
16
|
+
import { getActivationStats, getActiveMemories } from '../memory/activation.js';
|
|
17
|
+
import { detectContradictions, getContradictionsFor } from '../memory/contradiction.js';
|
|
18
|
+
import { enrichMemory } from '../memory/store.js';
|
|
19
|
+
import { memoryEvents, emitDecayTick, emitConsolidation, getUnprocessedEvents, markEventsProcessed, cleanupOldEvents, } from './events.js';
|
|
20
|
+
import { BrainWorker } from '../worker/brain-worker.js';
|
|
21
|
+
import { pause, resume, getControlStatus } from './control.js';
|
|
22
|
+
import { getCurrentVersion, checkForUpdates, performUpdate, scheduleRestart } from './version.js';
|
|
23
|
+
const PORT = process.env.PORT || 3001;
|
|
24
|
+
// Track connected WebSocket clients
|
|
25
|
+
const clients = new Set();
|
|
26
|
+
/**
|
|
27
|
+
* Start the visualization API server
|
|
28
|
+
*/
|
|
29
|
+
export function startVisualizationServer(dbPath) {
|
|
30
|
+
// Initialize database
|
|
31
|
+
initDatabase(dbPath || DEFAULT_CONFIG.dbPath);
|
|
32
|
+
const app = express();
|
|
33
|
+
const server = createServer(app);
|
|
34
|
+
// Middleware — CORS restricted to localhost by default
|
|
35
|
+
const allowedOrigins = process.env.CORTEX_CORS_ORIGINS
|
|
36
|
+
? process.env.CORTEX_CORS_ORIGINS.split(',').map(s => s.trim())
|
|
37
|
+
: ['http://localhost:3030', 'http://localhost:3000', 'http://127.0.0.1:3030', 'http://127.0.0.1:3000'];
|
|
38
|
+
app.use(cors({
|
|
39
|
+
origin: (origin, callback) => {
|
|
40
|
+
// Allow requests with no origin (curl, server-to-server, same-origin)
|
|
41
|
+
if (!origin || allowedOrigins.includes(origin)) {
|
|
42
|
+
callback(null, true);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
callback(new Error(`Origin ${origin} not allowed by CORS`));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
app.use(express.json());
|
|
50
|
+
// ============================================
|
|
51
|
+
// REST API ENDPOINTS
|
|
52
|
+
// ============================================
|
|
53
|
+
// Health check
|
|
54
|
+
app.get('/api/health', (_req, res) => {
|
|
55
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
56
|
+
});
|
|
57
|
+
// Get all memories with filters and pagination
|
|
58
|
+
app.get('/api/memories', async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
// Extract query params as strings
|
|
61
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
62
|
+
const type = typeof req.query.type === 'string' ? req.query.type : undefined;
|
|
63
|
+
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
|
|
64
|
+
const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '50';
|
|
65
|
+
const offsetStr = typeof req.query.offset === 'string' ? req.query.offset : '0';
|
|
66
|
+
const mode = typeof req.query.mode === 'string' ? req.query.mode : 'recent';
|
|
67
|
+
const query = typeof req.query.query === 'string' ? req.query.query : undefined;
|
|
68
|
+
const limit = Math.min(parseInt(limitStr), 200); // Cap at 200
|
|
69
|
+
const offset = parseInt(offsetStr);
|
|
70
|
+
let memories;
|
|
71
|
+
if (mode === 'search' && query) {
|
|
72
|
+
const results = await searchMemories({
|
|
73
|
+
query,
|
|
74
|
+
project,
|
|
75
|
+
type: type,
|
|
76
|
+
category: category,
|
|
77
|
+
limit: limit + offset + 1, // Fetch extra to check hasMore
|
|
78
|
+
});
|
|
79
|
+
memories = results.map(r => r.memory);
|
|
80
|
+
}
|
|
81
|
+
else if (mode === 'important') {
|
|
82
|
+
memories = getHighPriorityMemories(limit + offset + 1, project);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
memories = getRecentMemories(limit + offset + 1, project);
|
|
86
|
+
}
|
|
87
|
+
// Filter by type and category if provided
|
|
88
|
+
if (type) {
|
|
89
|
+
memories = memories.filter(m => m.type === type);
|
|
90
|
+
}
|
|
91
|
+
if (category) {
|
|
92
|
+
memories = memories.filter(m => m.category === category);
|
|
93
|
+
}
|
|
94
|
+
// Get total count for pagination
|
|
95
|
+
const stats = getMemoryStats(project);
|
|
96
|
+
const total = stats.total;
|
|
97
|
+
// Apply pagination
|
|
98
|
+
const hasMore = memories.length > offset + limit;
|
|
99
|
+
const paginatedMemories = memories.slice(offset, offset + limit);
|
|
100
|
+
// Add computed decayed score to each memory
|
|
101
|
+
const memoriesWithDecay = paginatedMemories.map(m => ({
|
|
102
|
+
...m,
|
|
103
|
+
decayedScore: calculateDecayedScore(m),
|
|
104
|
+
}));
|
|
105
|
+
res.json({
|
|
106
|
+
memories: memoriesWithDecay,
|
|
107
|
+
pagination: {
|
|
108
|
+
offset,
|
|
109
|
+
limit,
|
|
110
|
+
total,
|
|
111
|
+
hasMore,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
res.status(500).json({ error: error.message });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Activity data for heatmap (must be before :id route)
|
|
120
|
+
app.get('/api/memories/activity', (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
123
|
+
const db = getDatabase();
|
|
124
|
+
const query = project
|
|
125
|
+
? `SELECT date(created_at) as date, COUNT(*) as count
|
|
126
|
+
FROM memories WHERE project = ?
|
|
127
|
+
GROUP BY date(created_at)
|
|
128
|
+
ORDER BY date DESC
|
|
129
|
+
LIMIT 365`
|
|
130
|
+
: `SELECT date(created_at) as date, COUNT(*) as count
|
|
131
|
+
FROM memories
|
|
132
|
+
GROUP BY date(created_at)
|
|
133
|
+
ORDER BY date DESC
|
|
134
|
+
LIMIT 365`;
|
|
135
|
+
const rows = project
|
|
136
|
+
? db.prepare(query).all(project)
|
|
137
|
+
: db.prepare(query).all();
|
|
138
|
+
res.json({ activity: rows });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
res.status(500).json({ error: error.message });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// Memory quality analysis (must be before :id route)
|
|
145
|
+
app.get('/api/memories/quality', (req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
148
|
+
const db = getDatabase();
|
|
149
|
+
const projectFilter = project ? 'AND project = ?' : '';
|
|
150
|
+
const params = project ? [project] : [];
|
|
151
|
+
const neverAccessed = db.prepare(`
|
|
152
|
+
SELECT id, title, category, type, created_at, salience
|
|
153
|
+
FROM memories WHERE access_count = 0 ${projectFilter}
|
|
154
|
+
AND created_at < datetime('now', '-1 day')
|
|
155
|
+
ORDER BY created_at DESC LIMIT 50
|
|
156
|
+
`).all(...params);
|
|
157
|
+
const stale = db.prepare(`
|
|
158
|
+
SELECT id, title, category, type, last_accessed, decayed_score, salience
|
|
159
|
+
FROM memories WHERE decayed_score < 0.3 ${projectFilter}
|
|
160
|
+
AND last_accessed < datetime('now', '-30 days')
|
|
161
|
+
ORDER BY decayed_score ASC LIMIT 50
|
|
162
|
+
`).all(...params);
|
|
163
|
+
const duplicates = db.prepare(`
|
|
164
|
+
SELECT m1.id as id1, m1.title as title_a, m2.id as id2, m2.title as title_b
|
|
165
|
+
FROM memories m1
|
|
166
|
+
JOIN memories m2 ON m1.title = m2.title AND m1.id < m2.id
|
|
167
|
+
${project ? 'WHERE m1.project = ?' : ''}
|
|
168
|
+
LIMIT 50
|
|
169
|
+
`).all(...params);
|
|
170
|
+
res.json({
|
|
171
|
+
neverAccessed: { count: neverAccessed.length, items: neverAccessed },
|
|
172
|
+
stale: { count: stale.length, items: stale },
|
|
173
|
+
duplicates: { count: duplicates.length, items: duplicates },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
res.status(500).json({ error: error.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// Get single memory by ID
|
|
181
|
+
app.get('/api/memories/:id', (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const id = parseInt(req.params.id);
|
|
184
|
+
const memory = getMemoryById(id);
|
|
185
|
+
if (!memory) {
|
|
186
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
187
|
+
}
|
|
188
|
+
res.json({
|
|
189
|
+
...memory,
|
|
190
|
+
decayedScore: calculateDecayedScore(memory),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
res.status(500).json({ error: error.message });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// Create memory
|
|
198
|
+
app.post('/api/memories', (req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const { title, content, type, category, project, tags, salience } = req.body;
|
|
201
|
+
if (!title || !content) {
|
|
202
|
+
return res.status(400).json({ error: 'Title and content required' });
|
|
203
|
+
}
|
|
204
|
+
const memory = addMemory({
|
|
205
|
+
title,
|
|
206
|
+
content,
|
|
207
|
+
type: type || 'short_term',
|
|
208
|
+
category: category || 'note',
|
|
209
|
+
project,
|
|
210
|
+
tags: tags || [],
|
|
211
|
+
salience,
|
|
212
|
+
});
|
|
213
|
+
res.status(201).json(memory);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// Handle paused state gracefully
|
|
217
|
+
if (error.name === 'MemoryPausedError') {
|
|
218
|
+
return res.status(503).json({
|
|
219
|
+
error: 'Memory creation is paused',
|
|
220
|
+
paused: true,
|
|
221
|
+
message: 'Use the dashboard control panel to resume memory creation.',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
res.status(500).json({ error: error.message });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// Delete memory
|
|
228
|
+
app.delete('/api/memories/:id', (req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
const id = parseInt(req.params.id);
|
|
231
|
+
const success = deleteMemory(id);
|
|
232
|
+
if (!success) {
|
|
233
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
234
|
+
}
|
|
235
|
+
res.json({ success: true });
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
res.status(500).json({ error: error.message });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
// Access/reinforce memory
|
|
242
|
+
app.post('/api/memories/:id/access', (req, res) => {
|
|
243
|
+
try {
|
|
244
|
+
const id = parseInt(req.params.id);
|
|
245
|
+
const memory = accessMemory(id);
|
|
246
|
+
if (!memory) {
|
|
247
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
248
|
+
}
|
|
249
|
+
res.json({
|
|
250
|
+
...memory,
|
|
251
|
+
decayedScore: calculateDecayedScore(memory),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
res.status(500).json({ error: error.message });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// Get statistics
|
|
259
|
+
app.get('/api/stats', (req, res) => {
|
|
260
|
+
try {
|
|
261
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
262
|
+
const stats = getMemoryStats(project);
|
|
263
|
+
// Add decay distribution
|
|
264
|
+
const db = getDatabase();
|
|
265
|
+
const rawRows = db.prepare(project
|
|
266
|
+
? 'SELECT * FROM memories WHERE project = ?'
|
|
267
|
+
: 'SELECT * FROM memories').all(project ? [project] : []);
|
|
268
|
+
// Convert raw DB rows to Memory objects (snake_case -> camelCase)
|
|
269
|
+
const allMemories = rawRows.map(rowToMemory);
|
|
270
|
+
const decayDistribution = {
|
|
271
|
+
healthy: 0, // > 0.35 (realistic given base salience 0.25 + access bonus)
|
|
272
|
+
fading: 0, // 0.2 - 0.35
|
|
273
|
+
critical: 0, // < 0.2 (approaching deletion threshold)
|
|
274
|
+
};
|
|
275
|
+
for (const m of allMemories) {
|
|
276
|
+
const score = calculateDecayedScore(m);
|
|
277
|
+
if (score > 0.35)
|
|
278
|
+
decayDistribution.healthy++;
|
|
279
|
+
else if (score > 0.2)
|
|
280
|
+
decayDistribution.fading++;
|
|
281
|
+
else
|
|
282
|
+
decayDistribution.critical++;
|
|
283
|
+
}
|
|
284
|
+
// Get spreading activation stats (Phase 2 organic feature)
|
|
285
|
+
const activationStats = getActivationStats();
|
|
286
|
+
res.json({
|
|
287
|
+
...stats,
|
|
288
|
+
decayDistribution,
|
|
289
|
+
activation: activationStats,
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
res.status(500).json({ error: error.message });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
// Get currently activated memories (spreading activation)
|
|
298
|
+
app.get('/api/activation', (_req, res) => {
|
|
299
|
+
try {
|
|
300
|
+
const activeMemories = getActiveMemories();
|
|
301
|
+
const stats = getActivationStats();
|
|
302
|
+
res.json({
|
|
303
|
+
activeMemories,
|
|
304
|
+
stats,
|
|
305
|
+
timestamp: new Date().toISOString(),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
res.status(500).json({ error: error.message });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
// ============================================
|
|
313
|
+
// ORGANIC BRAIN ENDPOINTS (Phase 3)
|
|
314
|
+
// ============================================
|
|
315
|
+
// Get detected contradictions
|
|
316
|
+
app.get('/api/contradictions', (req, res) => {
|
|
317
|
+
try {
|
|
318
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
319
|
+
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
|
|
320
|
+
const minScoreStr = typeof req.query.minScore === 'string' ? req.query.minScore : '0.4';
|
|
321
|
+
const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '20';
|
|
322
|
+
const minScore = parseFloat(minScoreStr);
|
|
323
|
+
const limit = parseInt(limitStr);
|
|
324
|
+
const contradictions = detectContradictions({
|
|
325
|
+
project,
|
|
326
|
+
category: category,
|
|
327
|
+
minScore,
|
|
328
|
+
limit,
|
|
329
|
+
});
|
|
330
|
+
res.json({
|
|
331
|
+
contradictions: contradictions.map(c => ({
|
|
332
|
+
memoryAId: c.memoryA.id,
|
|
333
|
+
memoryATitle: c.memoryA.title,
|
|
334
|
+
memoryBId: c.memoryB.id,
|
|
335
|
+
memoryBTitle: c.memoryB.title,
|
|
336
|
+
score: c.score,
|
|
337
|
+
reason: c.reason,
|
|
338
|
+
sharedTopics: c.sharedTopics,
|
|
339
|
+
})),
|
|
340
|
+
count: contradictions.length,
|
|
341
|
+
timestamp: new Date().toISOString(),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
res.status(500).json({ error: error.message });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
// Get contradictions for a specific memory
|
|
349
|
+
app.get('/api/memories/:id/contradictions', (req, res) => {
|
|
350
|
+
try {
|
|
351
|
+
const id = parseInt(req.params.id);
|
|
352
|
+
if (isNaN(id)) {
|
|
353
|
+
return res.status(400).json({ error: 'Invalid memory ID' });
|
|
354
|
+
}
|
|
355
|
+
const contradictions = getContradictionsFor(id);
|
|
356
|
+
res.json({
|
|
357
|
+
memoryId: id,
|
|
358
|
+
contradictions: contradictions.map(c => ({
|
|
359
|
+
contradictingMemoryId: c.memoryB.id,
|
|
360
|
+
contradictingMemoryTitle: c.memoryB.title,
|
|
361
|
+
score: c.score,
|
|
362
|
+
reason: c.reason,
|
|
363
|
+
sharedTopics: c.sharedTopics,
|
|
364
|
+
})),
|
|
365
|
+
count: contradictions.length,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
res.status(500).json({ error: error.message });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
// Manually enrich a memory with new context
|
|
373
|
+
app.post('/api/memories/:id/enrich', (req, res) => {
|
|
374
|
+
try {
|
|
375
|
+
const id = parseInt(req.params.id);
|
|
376
|
+
if (isNaN(id)) {
|
|
377
|
+
return res.status(400).json({ error: 'Invalid memory ID' });
|
|
378
|
+
}
|
|
379
|
+
const { context, contextType } = req.body;
|
|
380
|
+
if (!context || typeof context !== 'string') {
|
|
381
|
+
return res.status(400).json({ error: 'Context string required in request body' });
|
|
382
|
+
}
|
|
383
|
+
const validTypes = ['search', 'access', 'related'];
|
|
384
|
+
const type = validTypes.includes(contextType) ? contextType : 'access';
|
|
385
|
+
const result = enrichMemory(id, context, type);
|
|
386
|
+
res.json(result);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
res.status(500).json({ error: error.message });
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
// Get list of all projects
|
|
393
|
+
app.get('/api/projects', (_req, res) => {
|
|
394
|
+
try {
|
|
395
|
+
const db = getDatabase();
|
|
396
|
+
const projects = db.prepare(`
|
|
397
|
+
SELECT DISTINCT project, COUNT(*) as memory_count
|
|
398
|
+
FROM memories
|
|
399
|
+
WHERE project IS NOT NULL AND project != ''
|
|
400
|
+
GROUP BY project
|
|
401
|
+
ORDER BY memory_count DESC
|
|
402
|
+
`).all();
|
|
403
|
+
// Add "All Projects" option with total count
|
|
404
|
+
const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get();
|
|
405
|
+
res.json({
|
|
406
|
+
projects: [
|
|
407
|
+
{ project: null, memory_count: totalCount.count, label: 'All Projects' },
|
|
408
|
+
...projects.map(p => ({ ...p, label: p.project })),
|
|
409
|
+
],
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
res.status(500).json({ error: error.message });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
// ============================================
|
|
417
|
+
// CONTROL ENDPOINTS
|
|
418
|
+
// ============================================
|
|
419
|
+
// Get control status
|
|
420
|
+
app.get('/api/control/status', (_req, res) => {
|
|
421
|
+
try {
|
|
422
|
+
const status = getControlStatus();
|
|
423
|
+
res.json(status);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
res.status(500).json({ error: error.message });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
// Pause memory creation
|
|
430
|
+
app.post('/api/control/pause', (_req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
pause();
|
|
433
|
+
res.json({ paused: true, message: 'Memory creation paused' });
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
res.status(500).json({ error: error.message });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
// Resume memory creation
|
|
440
|
+
app.post('/api/control/resume', (_req, res) => {
|
|
441
|
+
try {
|
|
442
|
+
resume();
|
|
443
|
+
res.json({ paused: false, message: 'Memory creation resumed' });
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
res.status(500).json({ error: error.message });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
// ============================================
|
|
450
|
+
// VERSION ENDPOINTS
|
|
451
|
+
// ============================================
|
|
452
|
+
// Get current version
|
|
453
|
+
app.get('/api/version', (_req, res) => {
|
|
454
|
+
try {
|
|
455
|
+
const version = getCurrentVersion();
|
|
456
|
+
res.json({ version });
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
res.status(500).json({ error: error.message });
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// Check for updates
|
|
463
|
+
app.get('/api/version/check', async (req, res) => {
|
|
464
|
+
try {
|
|
465
|
+
const forceRefresh = req.query.force === 'true';
|
|
466
|
+
const versionInfo = await checkForUpdates(forceRefresh);
|
|
467
|
+
res.json(versionInfo);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
res.status(500).json({ error: error.message });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
// Perform update
|
|
474
|
+
app.post('/api/version/update', async (_req, res) => {
|
|
475
|
+
try {
|
|
476
|
+
// Notify clients that update is starting
|
|
477
|
+
broadcast({
|
|
478
|
+
type: 'update_started',
|
|
479
|
+
timestamp: new Date().toISOString(),
|
|
480
|
+
data: { message: 'Update in progress...' },
|
|
481
|
+
});
|
|
482
|
+
const result = await performUpdate();
|
|
483
|
+
// Notify clients of result
|
|
484
|
+
broadcast({
|
|
485
|
+
type: result.success ? 'update_complete' : 'update_failed',
|
|
486
|
+
timestamp: new Date().toISOString(),
|
|
487
|
+
data: result,
|
|
488
|
+
});
|
|
489
|
+
res.json(result);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
res.status(500).json({ error: error.message });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
// Restart server
|
|
496
|
+
app.post('/api/version/restart', (_req, res) => {
|
|
497
|
+
try {
|
|
498
|
+
// Notify all WebSocket clients
|
|
499
|
+
broadcast({
|
|
500
|
+
type: 'server_restarting',
|
|
501
|
+
timestamp: new Date().toISOString(),
|
|
502
|
+
data: { message: 'Server restarting in 3 seconds...' },
|
|
503
|
+
});
|
|
504
|
+
// Close WebSocket connections gracefully
|
|
505
|
+
for (const client of clients) {
|
|
506
|
+
client.send(JSON.stringify({
|
|
507
|
+
type: 'server_restarting',
|
|
508
|
+
timestamp: new Date().toISOString(),
|
|
509
|
+
data: { reconnectIn: 5000 },
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
// Schedule restart after response is sent
|
|
513
|
+
res.json({ success: true, message: 'Server will restart in 3 seconds' });
|
|
514
|
+
scheduleRestart(3000);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
res.status(500).json({ error: error.message });
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// Get memory links/relationships
|
|
521
|
+
app.get('/api/links', (req, res) => {
|
|
522
|
+
try {
|
|
523
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
524
|
+
const db = getDatabase();
|
|
525
|
+
const query = project
|
|
526
|
+
? `
|
|
527
|
+
SELECT
|
|
528
|
+
ml.*,
|
|
529
|
+
m1.title as source_title,
|
|
530
|
+
m1.category as source_category,
|
|
531
|
+
m1.type as source_type,
|
|
532
|
+
m2.title as target_title,
|
|
533
|
+
m2.category as target_category,
|
|
534
|
+
m2.type as target_type
|
|
535
|
+
FROM memory_links ml
|
|
536
|
+
JOIN memories m1 ON ml.source_id = m1.id
|
|
537
|
+
JOIN memories m2 ON ml.target_id = m2.id
|
|
538
|
+
WHERE m1.project = ? OR m2.project = ?
|
|
539
|
+
ORDER BY ml.created_at DESC
|
|
540
|
+
LIMIT 500
|
|
541
|
+
`
|
|
542
|
+
: `
|
|
543
|
+
SELECT
|
|
544
|
+
ml.*,
|
|
545
|
+
m1.title as source_title,
|
|
546
|
+
m1.category as source_category,
|
|
547
|
+
m1.type as source_type,
|
|
548
|
+
m2.title as target_title,
|
|
549
|
+
m2.category as target_category,
|
|
550
|
+
m2.type as target_type
|
|
551
|
+
FROM memory_links ml
|
|
552
|
+
JOIN memories m1 ON ml.source_id = m1.id
|
|
553
|
+
JOIN memories m2 ON ml.target_id = m2.id
|
|
554
|
+
ORDER BY ml.created_at DESC
|
|
555
|
+
LIMIT 500
|
|
556
|
+
`;
|
|
557
|
+
const links = project
|
|
558
|
+
? db.prepare(query).all(project, project)
|
|
559
|
+
: db.prepare(query).all();
|
|
560
|
+
res.json(links);
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
res.status(500).json({ error: error.message });
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
// ============================================
|
|
567
|
+
// INSIGHTS ENDPOINTS
|
|
568
|
+
// ============================================
|
|
569
|
+
// (activity and quality routes moved above :id route)
|
|
570
|
+
// ============================================
|
|
571
|
+
// SQL CONSOLE ENDPOINT
|
|
572
|
+
// ============================================
|
|
573
|
+
// Execute SQL query (with safety restrictions)
|
|
574
|
+
app.post('/api/sql', (req, res) => {
|
|
575
|
+
try {
|
|
576
|
+
const { query, allowWrite } = req.body;
|
|
577
|
+
if (!query || typeof query !== 'string') {
|
|
578
|
+
return res.status(400).json({ error: 'Query string required' });
|
|
579
|
+
}
|
|
580
|
+
const upperQuery = query.toUpperCase().trim();
|
|
581
|
+
// Always block DROP and TRUNCATE
|
|
582
|
+
if (/\bDROP\b/.test(upperQuery) || /\bTRUNCATE\b/.test(upperQuery)) {
|
|
583
|
+
return res.status(403).json({
|
|
584
|
+
error: 'DROP and TRUNCATE operations are blocked for safety',
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
// Block writes unless explicitly allowed
|
|
588
|
+
const isWriteOperation = upperQuery.startsWith('INSERT') ||
|
|
589
|
+
upperQuery.startsWith('UPDATE') ||
|
|
590
|
+
upperQuery.startsWith('DELETE') ||
|
|
591
|
+
upperQuery.startsWith('ALTER') ||
|
|
592
|
+
upperQuery.startsWith('CREATE');
|
|
593
|
+
if (isWriteOperation && !allowWrite) {
|
|
594
|
+
return res.status(403).json({
|
|
595
|
+
error: 'Write operations are disabled. Enable allowWrite to execute.',
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
const db = getDatabase();
|
|
599
|
+
const startTime = Date.now();
|
|
600
|
+
// Execute query
|
|
601
|
+
const isSelect = upperQuery.startsWith('SELECT') || upperQuery.startsWith('PRAGMA');
|
|
602
|
+
if (isSelect) {
|
|
603
|
+
const rows = db.prepare(query).all();
|
|
604
|
+
const executionTime = Date.now() - startTime;
|
|
605
|
+
// Get column names from first row or empty
|
|
606
|
+
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
607
|
+
res.json({
|
|
608
|
+
columns,
|
|
609
|
+
rows,
|
|
610
|
+
rowCount: rows.length,
|
|
611
|
+
executionTime,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
// Write operation
|
|
616
|
+
const result = db.prepare(query).run();
|
|
617
|
+
const executionTime = Date.now() - startTime;
|
|
618
|
+
res.json({
|
|
619
|
+
columns: ['changes', 'lastInsertRowid'],
|
|
620
|
+
rows: [{ changes: result.changes, lastInsertRowid: result.lastInsertRowid }],
|
|
621
|
+
rowCount: 1,
|
|
622
|
+
executionTime,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
res.status(500).json({ error: error.message });
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
// Trigger consolidation
|
|
631
|
+
app.post('/api/consolidate', (_req, res) => {
|
|
632
|
+
try {
|
|
633
|
+
const result = consolidate();
|
|
634
|
+
// Emit event for Activity log
|
|
635
|
+
emitConsolidation(result);
|
|
636
|
+
res.json({
|
|
637
|
+
success: true,
|
|
638
|
+
...result,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
res.status(500).json({ error: error.message });
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
// Get context summary
|
|
646
|
+
app.get('/api/context', async (req, res) => {
|
|
647
|
+
try {
|
|
648
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
649
|
+
const summary = await generateContextSummary(project);
|
|
650
|
+
const formatted = formatContextSummary(summary);
|
|
651
|
+
res.json({
|
|
652
|
+
summary,
|
|
653
|
+
formatted,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
res.status(500).json({ error: error.message });
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
// Get search suggestions (for autocomplete)
|
|
661
|
+
app.get('/api/suggestions', (req, res) => {
|
|
662
|
+
try {
|
|
663
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
664
|
+
const limit = typeof req.query.limit === 'string' ? parseInt(req.query.limit) : 10;
|
|
665
|
+
if (!query || query.length < 2) {
|
|
666
|
+
return res.json({ suggestions: [] });
|
|
667
|
+
}
|
|
668
|
+
const db = getDatabase();
|
|
669
|
+
// Get suggestions from memory titles, categories, tags, and projects
|
|
670
|
+
const suggestions = [];
|
|
671
|
+
// Search titles that contain the query
|
|
672
|
+
const titleMatches = db.prepare(`
|
|
673
|
+
SELECT DISTINCT title, COUNT(*) as count
|
|
674
|
+
FROM memories
|
|
675
|
+
WHERE title LIKE ?
|
|
676
|
+
GROUP BY title
|
|
677
|
+
ORDER BY count DESC, last_accessed DESC
|
|
678
|
+
LIMIT ?
|
|
679
|
+
`).all(`%${query}%`, limit);
|
|
680
|
+
for (const match of titleMatches) {
|
|
681
|
+
suggestions.push({ text: match.title, type: 'title', count: match.count });
|
|
682
|
+
}
|
|
683
|
+
// Get matching categories
|
|
684
|
+
const categoryMatches = db.prepare(`
|
|
685
|
+
SELECT DISTINCT category, COUNT(*) as count
|
|
686
|
+
FROM memories
|
|
687
|
+
WHERE category LIKE ?
|
|
688
|
+
GROUP BY category
|
|
689
|
+
ORDER BY count DESC
|
|
690
|
+
LIMIT 5
|
|
691
|
+
`).all(`%${query}%`);
|
|
692
|
+
for (const match of categoryMatches) {
|
|
693
|
+
suggestions.push({ text: match.category, type: 'category', count: match.count });
|
|
694
|
+
}
|
|
695
|
+
// Get matching projects
|
|
696
|
+
const projectMatches = db.prepare(`
|
|
697
|
+
SELECT DISTINCT project, COUNT(*) as count
|
|
698
|
+
FROM memories
|
|
699
|
+
WHERE project IS NOT NULL AND project LIKE ?
|
|
700
|
+
GROUP BY project
|
|
701
|
+
ORDER BY count DESC
|
|
702
|
+
LIMIT 5
|
|
703
|
+
`).all(`%${query}%`);
|
|
704
|
+
for (const match of projectMatches) {
|
|
705
|
+
suggestions.push({ text: match.project, type: 'project', count: match.count });
|
|
706
|
+
}
|
|
707
|
+
// Sort by count and limit total results
|
|
708
|
+
suggestions.sort((a, b) => b.count - a.count);
|
|
709
|
+
const limitedSuggestions = suggestions.slice(0, limit);
|
|
710
|
+
res.json({ suggestions: limitedSuggestions });
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
res.status(500).json({ error: error.message });
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
// ============================================
|
|
717
|
+
// GRAPH / ONTOLOGY ENDPOINTS
|
|
718
|
+
// ============================================
|
|
719
|
+
// List entities with optional filters and pagination
|
|
720
|
+
app.get('/api/graph/entities', (req, res) => {
|
|
721
|
+
try {
|
|
722
|
+
const db = getDatabase();
|
|
723
|
+
const type = typeof req.query.type === 'string' ? req.query.type : undefined;
|
|
724
|
+
const minMentions = typeof req.query.minMentions === 'string' ? parseInt(req.query.minMentions) : 0;
|
|
725
|
+
const limit = typeof req.query.limit === 'string' ? Math.min(parseInt(req.query.limit), 500) : 100;
|
|
726
|
+
const offset = typeof req.query.offset === 'string' ? parseInt(req.query.offset) : 0;
|
|
727
|
+
let whereClause = 'WHERE 1=1';
|
|
728
|
+
const params = [];
|
|
729
|
+
if (type) {
|
|
730
|
+
whereClause += ' AND type = ?';
|
|
731
|
+
params.push(type);
|
|
732
|
+
}
|
|
733
|
+
if (minMentions > 0) {
|
|
734
|
+
whereClause += ' AND memory_count >= ?';
|
|
735
|
+
params.push(minMentions);
|
|
736
|
+
}
|
|
737
|
+
const totalRow = db.prepare(`SELECT COUNT(*) as count FROM entities ${whereClause}`).get(...params);
|
|
738
|
+
const total = totalRow.count;
|
|
739
|
+
const rows = db.prepare(`SELECT * FROM entities ${whereClause} ORDER BY memory_count DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
740
|
+
const entities = rows.map((r) => {
|
|
741
|
+
let aliases = [];
|
|
742
|
+
try {
|
|
743
|
+
aliases = JSON.parse(r.aliases || '[]');
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
aliases = [];
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
id: r.id,
|
|
750
|
+
name: r.name,
|
|
751
|
+
type: r.type,
|
|
752
|
+
memoryCount: r.memory_count ?? 0,
|
|
753
|
+
aliases,
|
|
754
|
+
createdAt: r.created_at,
|
|
755
|
+
updatedAt: r.updated_at,
|
|
756
|
+
};
|
|
757
|
+
});
|
|
758
|
+
res.json({ entities, total, offset, limit, hasMore: offset + limit < total });
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
res.status(500).json({ error: error.message });
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
// Get triples for a specific entity
|
|
765
|
+
app.get('/api/graph/entities/:id/triples', (req, res) => {
|
|
766
|
+
try {
|
|
767
|
+
const db = getDatabase();
|
|
768
|
+
const id = parseInt(req.params.id);
|
|
769
|
+
if (isNaN(id)) {
|
|
770
|
+
return res.status(400).json({ error: 'Invalid entity ID' });
|
|
771
|
+
}
|
|
772
|
+
const rows = db.prepare(`
|
|
773
|
+
SELECT t.*, s.name as subject_name, s.type as subject_type,
|
|
774
|
+
o.name as object_name, o.type as object_type
|
|
775
|
+
FROM triples t
|
|
776
|
+
JOIN entities s ON s.id = t.subject_id
|
|
777
|
+
JOIN entities o ON o.id = t.object_id
|
|
778
|
+
WHERE t.subject_id = ? OR t.object_id = ?
|
|
779
|
+
ORDER BY t.created_at DESC
|
|
780
|
+
`).all(id, id);
|
|
781
|
+
res.json({ triples: rows });
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
res.status(500).json({ error: error.message });
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
// Get memories linked to a specific entity
|
|
788
|
+
app.get('/api/graph/entities/:id/memories', (req, res) => {
|
|
789
|
+
try {
|
|
790
|
+
const db = getDatabase();
|
|
791
|
+
const id = parseInt(req.params.id);
|
|
792
|
+
if (isNaN(id)) {
|
|
793
|
+
return res.status(400).json({ error: 'Invalid entity ID' });
|
|
794
|
+
}
|
|
795
|
+
const rows = db.prepare(`
|
|
796
|
+
SELECT m.id, m.title, m.type, m.category, m.salience, m.created_at
|
|
797
|
+
FROM memories m
|
|
798
|
+
JOIN memory_entities me ON me.memory_id = m.id
|
|
799
|
+
WHERE me.entity_id = ?
|
|
800
|
+
ORDER BY m.salience DESC, m.created_at DESC
|
|
801
|
+
LIMIT 50
|
|
802
|
+
`).all(id);
|
|
803
|
+
res.json({ memories: rows });
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
res.status(500).json({ error: error.message });
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
// List triples with optional predicate filter and pagination
|
|
810
|
+
app.get('/api/graph/triples', (req, res) => {
|
|
811
|
+
try {
|
|
812
|
+
const db = getDatabase();
|
|
813
|
+
const predicate = typeof req.query.predicate === 'string' ? req.query.predicate : undefined;
|
|
814
|
+
const limit = typeof req.query.limit === 'string' ? Math.min(parseInt(req.query.limit), 500) : 100;
|
|
815
|
+
const offset = typeof req.query.offset === 'string' ? parseInt(req.query.offset) : 0;
|
|
816
|
+
let whereClause = '';
|
|
817
|
+
const params = [];
|
|
818
|
+
if (predicate) {
|
|
819
|
+
whereClause = 'WHERE t.predicate = ?';
|
|
820
|
+
params.push(predicate);
|
|
821
|
+
}
|
|
822
|
+
const totalRow = db.prepare(`SELECT COUNT(*) as count FROM triples t ${whereClause}`).get(...params);
|
|
823
|
+
const total = totalRow.count;
|
|
824
|
+
const rows = db.prepare(`
|
|
825
|
+
SELECT t.*, s.name as subject_name, s.type as subject_type,
|
|
826
|
+
o.name as object_name, o.type as object_type
|
|
827
|
+
FROM triples t
|
|
828
|
+
JOIN entities s ON s.id = t.subject_id
|
|
829
|
+
JOIN entities o ON o.id = t.object_id
|
|
830
|
+
${whereClause}
|
|
831
|
+
ORDER BY t.created_at DESC
|
|
832
|
+
LIMIT ? OFFSET ?
|
|
833
|
+
`).all(...params, limit, offset);
|
|
834
|
+
res.json({ triples: rows, total, offset, limit, hasMore: offset + limit < total });
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
res.status(500).json({ error: error.message });
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
// Search entities by name
|
|
841
|
+
app.get('/api/graph/search', (req, res) => {
|
|
842
|
+
try {
|
|
843
|
+
const db = getDatabase();
|
|
844
|
+
const q = typeof req.query.q === 'string' ? req.query.q : '';
|
|
845
|
+
if (!q) {
|
|
846
|
+
return res.status(400).json({ error: 'Query parameter "q" is required' });
|
|
847
|
+
}
|
|
848
|
+
const rows = db.prepare(`SELECT * FROM entities WHERE LOWER(name) LIKE ? ORDER BY memory_count DESC LIMIT 20`).all(`%${q.toLowerCase()}%`);
|
|
849
|
+
const entities = rows.map((r) => {
|
|
850
|
+
let aliases = [];
|
|
851
|
+
try {
|
|
852
|
+
aliases = JSON.parse(r.aliases || '[]');
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
aliases = [];
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
id: r.id,
|
|
859
|
+
name: r.name,
|
|
860
|
+
type: r.type,
|
|
861
|
+
memoryCount: r.memory_count ?? 0,
|
|
862
|
+
aliases,
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
res.json({ entities });
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
res.status(500).json({ error: error.message });
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
// Find path between two entities using BFS
|
|
872
|
+
app.get('/api/graph/paths', (req, res) => {
|
|
873
|
+
try {
|
|
874
|
+
const db = getDatabase();
|
|
875
|
+
const fromName = typeof req.query.from === 'string' ? req.query.from : '';
|
|
876
|
+
const toName = typeof req.query.to === 'string' ? req.query.to : '';
|
|
877
|
+
if (!fromName || !toName) {
|
|
878
|
+
return res.status(400).json({ error: 'Both "from" and "to" query parameters are required' });
|
|
879
|
+
}
|
|
880
|
+
const fromRow = db.prepare('SELECT * FROM entities WHERE LOWER(name) = LOWER(?)').get(fromName);
|
|
881
|
+
if (!fromRow) {
|
|
882
|
+
return res.status(404).json({ error: `Entity "${fromName}" not found` });
|
|
883
|
+
}
|
|
884
|
+
const toRow = db.prepare('SELECT * FROM entities WHERE LOWER(name) = LOWER(?)').get(toName);
|
|
885
|
+
if (!toRow) {
|
|
886
|
+
return res.status(404).json({ error: `Entity "${toName}" not found` });
|
|
887
|
+
}
|
|
888
|
+
if (fromRow.id === toRow.id) {
|
|
889
|
+
return res.json({ path: [{ entity: fromRow.name, predicate: '(self)' }], sourceMemories: [] });
|
|
890
|
+
}
|
|
891
|
+
// BFS
|
|
892
|
+
const maxDepth = 4;
|
|
893
|
+
const visited = new Map();
|
|
894
|
+
visited.set(fromRow.id, { id: fromRow.id, name: fromRow.name, parentId: null, predicate: '', sourceMemoryId: null });
|
|
895
|
+
let frontier = [fromRow.id];
|
|
896
|
+
let found = false;
|
|
897
|
+
for (let d = 0; d < maxDepth && !found; d++) {
|
|
898
|
+
const nextFrontier = [];
|
|
899
|
+
for (const nodeId of frontier) {
|
|
900
|
+
const outgoing = db.prepare('SELECT t.object_id as next_id, t.predicate, t.source_memory_id, e.name FROM triples t JOIN entities e ON e.id = t.object_id WHERE t.subject_id = ?').all(nodeId);
|
|
901
|
+
for (const row of outgoing) {
|
|
902
|
+
if (!visited.has(row.next_id)) {
|
|
903
|
+
visited.set(row.next_id, { id: row.next_id, name: row.name, parentId: nodeId, predicate: row.predicate, sourceMemoryId: row.source_memory_id });
|
|
904
|
+
nextFrontier.push(row.next_id);
|
|
905
|
+
if (row.next_id === toRow.id) {
|
|
906
|
+
found = true;
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (found)
|
|
912
|
+
break;
|
|
913
|
+
const incoming = db.prepare('SELECT t.subject_id as next_id, t.predicate, t.source_memory_id, e.name FROM triples t JOIN entities e ON e.id = t.subject_id WHERE t.object_id = ?').all(nodeId);
|
|
914
|
+
for (const row of incoming) {
|
|
915
|
+
if (!visited.has(row.next_id)) {
|
|
916
|
+
visited.set(row.next_id, { id: row.next_id, name: row.name, parentId: nodeId, predicate: `~${row.predicate}`, sourceMemoryId: row.source_memory_id });
|
|
917
|
+
nextFrontier.push(row.next_id);
|
|
918
|
+
if (row.next_id === toRow.id) {
|
|
919
|
+
found = true;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (found)
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
frontier = nextFrontier;
|
|
928
|
+
if (frontier.length === 0)
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
if (!found) {
|
|
932
|
+
return res.json({ path: [], sourceMemories: [], message: 'No path found' });
|
|
933
|
+
}
|
|
934
|
+
// Reconstruct path
|
|
935
|
+
const path = [];
|
|
936
|
+
const sourceMemoryIds = [];
|
|
937
|
+
let current = visited.get(toRow.id);
|
|
938
|
+
while (current) {
|
|
939
|
+
path.unshift({ entity: current.name, predicate: current.predicate });
|
|
940
|
+
if (current.sourceMemoryId)
|
|
941
|
+
sourceMemoryIds.push(current.sourceMemoryId);
|
|
942
|
+
current = current.parentId !== null ? visited.get(current.parentId) : undefined;
|
|
943
|
+
}
|
|
944
|
+
// Fetch source memories
|
|
945
|
+
const sourceMemories = sourceMemoryIds.length > 0
|
|
946
|
+
? db.prepare(`SELECT id, title FROM memories WHERE id IN (${sourceMemoryIds.map(() => '?').join(',')})`).all(...sourceMemoryIds)
|
|
947
|
+
: [];
|
|
948
|
+
res.json({ path, sourceMemories });
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
res.status(500).json({ error: error.message });
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
// ============================================
|
|
955
|
+
// BRAIN WORKER (Phase 4)
|
|
956
|
+
// ============================================
|
|
957
|
+
// Create and start the background brain worker
|
|
958
|
+
const brainWorker = new BrainWorker();
|
|
959
|
+
// Worker status endpoint
|
|
960
|
+
app.get('/api/worker/status', (_req, res) => {
|
|
961
|
+
try {
|
|
962
|
+
res.json(brainWorker.getStatus());
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
res.status(500).json({ error: error.message });
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// Manually trigger light tick (for testing)
|
|
969
|
+
app.post('/api/worker/trigger-light', async (_req, res) => {
|
|
970
|
+
try {
|
|
971
|
+
const result = await brainWorker.triggerLightTick();
|
|
972
|
+
res.json({
|
|
973
|
+
success: true,
|
|
974
|
+
...result,
|
|
975
|
+
timestamp: result.timestamp.toISOString(),
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
res.status(500).json({ error: error.message });
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
// Manually trigger medium tick (for testing)
|
|
983
|
+
app.post('/api/worker/trigger-medium', async (_req, res) => {
|
|
984
|
+
try {
|
|
985
|
+
const result = await brainWorker.triggerMediumTick();
|
|
986
|
+
res.json({
|
|
987
|
+
success: true,
|
|
988
|
+
...result,
|
|
989
|
+
timestamp: result.timestamp.toISOString(),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
res.status(500).json({ error: error.message });
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
// ============================================
|
|
997
|
+
// WEBSOCKET SERVER
|
|
998
|
+
// ============================================
|
|
999
|
+
const wss = new WebSocketServer({ server, path: '/ws/events' });
|
|
1000
|
+
wss.on('connection', (ws) => {
|
|
1001
|
+
clients.add(ws);
|
|
1002
|
+
console.log(`[WS] Client connected. Total: ${clients.size}`);
|
|
1003
|
+
// Send initial state
|
|
1004
|
+
const stats = getMemoryStats();
|
|
1005
|
+
const memories = getRecentMemories(100);
|
|
1006
|
+
const memoriesWithDecay = memories.map(m => ({
|
|
1007
|
+
...m,
|
|
1008
|
+
decayedScore: calculateDecayedScore(m),
|
|
1009
|
+
}));
|
|
1010
|
+
ws.send(JSON.stringify({
|
|
1011
|
+
type: 'initial_state',
|
|
1012
|
+
timestamp: new Date().toISOString(),
|
|
1013
|
+
data: {
|
|
1014
|
+
stats,
|
|
1015
|
+
memories: memoriesWithDecay,
|
|
1016
|
+
},
|
|
1017
|
+
}));
|
|
1018
|
+
ws.on('close', () => {
|
|
1019
|
+
clients.delete(ws);
|
|
1020
|
+
console.log(`[WS] Client disconnected. Total: ${clients.size}`);
|
|
1021
|
+
});
|
|
1022
|
+
ws.on('error', (error) => {
|
|
1023
|
+
console.error('[WS] Error:', error);
|
|
1024
|
+
clients.delete(ws);
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
// Broadcast events to all connected clients
|
|
1028
|
+
function broadcast(event) {
|
|
1029
|
+
const message = JSON.stringify(event);
|
|
1030
|
+
for (const client of clients) {
|
|
1031
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1032
|
+
client.send(message);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
// Subscribe to memory events
|
|
1037
|
+
memoryEvents.onMemoryEvent((event) => {
|
|
1038
|
+
broadcast(event);
|
|
1039
|
+
});
|
|
1040
|
+
// Decay tick - update clients with decay changes every 30 seconds
|
|
1041
|
+
let decayTickCount = 0;
|
|
1042
|
+
setInterval(() => {
|
|
1043
|
+
const db = getDatabase();
|
|
1044
|
+
const rawRows = db.prepare('SELECT * FROM memories ORDER BY last_accessed DESC LIMIT 200').all();
|
|
1045
|
+
// Convert raw DB rows to Memory objects (snake_case -> camelCase)
|
|
1046
|
+
const memories = rawRows.map(rowToMemory);
|
|
1047
|
+
const updates = [];
|
|
1048
|
+
for (const memory of memories) {
|
|
1049
|
+
const newScore = calculateDecayedScore(memory);
|
|
1050
|
+
// Only include memories that have decayed significantly since last update
|
|
1051
|
+
// Compare to decayedScore (not salience) to detect actual changes
|
|
1052
|
+
if (Math.abs(newScore - memory.decayedScore) > 0.01) {
|
|
1053
|
+
updates.push({
|
|
1054
|
+
memoryId: memory.id,
|
|
1055
|
+
oldScore: memory.decayedScore,
|
|
1056
|
+
newScore,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (updates.length > 0) {
|
|
1061
|
+
emitDecayTick(updates);
|
|
1062
|
+
}
|
|
1063
|
+
// Persist decay scores and checkpoint WAL every 5 minutes (10 ticks)
|
|
1064
|
+
decayTickCount++;
|
|
1065
|
+
if (decayTickCount >= 10) {
|
|
1066
|
+
decayTickCount = 0;
|
|
1067
|
+
try {
|
|
1068
|
+
updateDecayScores();
|
|
1069
|
+
// Checkpoint WAL to prevent file bloat and reduce contention
|
|
1070
|
+
const checkpoint = checkpointWal();
|
|
1071
|
+
if (checkpoint.walPages > 0) {
|
|
1072
|
+
console.log(`[WAL] Checkpointed ${checkpoint.checkpointed}/${checkpoint.walPages} pages`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
console.error('[Maintenance] Failed to persist decay scores or checkpoint:', error);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}, 30000);
|
|
1080
|
+
// ============================================
|
|
1081
|
+
// CROSS-PROCESS EVENT POLLING (IPC)
|
|
1082
|
+
// ============================================
|
|
1083
|
+
// Poll database for events from MCP process every 500ms
|
|
1084
|
+
const eventPollInterval = setInterval(() => {
|
|
1085
|
+
try {
|
|
1086
|
+
const events = getUnprocessedEvents(50);
|
|
1087
|
+
if (events.length > 0) {
|
|
1088
|
+
const ids = [];
|
|
1089
|
+
for (const event of events) {
|
|
1090
|
+
broadcast({ type: event.type, data: event.data, timestamp: event.timestamp });
|
|
1091
|
+
ids.push(event.id);
|
|
1092
|
+
}
|
|
1093
|
+
markEventsProcessed(ids);
|
|
1094
|
+
console.log(`[Events] Processed ${events.length} cross-process events`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
catch (error) {
|
|
1098
|
+
// Don't spam logs on transient errors
|
|
1099
|
+
if (Math.random() < 0.1) {
|
|
1100
|
+
console.error('[Events] Event polling error:', error);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}, 500);
|
|
1104
|
+
// Cleanup old processed events every hour
|
|
1105
|
+
const cleanupInterval = setInterval(() => {
|
|
1106
|
+
try {
|
|
1107
|
+
cleanupOldEvents();
|
|
1108
|
+
}
|
|
1109
|
+
catch (error) {
|
|
1110
|
+
console.error('[Events] Cleanup error:', error);
|
|
1111
|
+
}
|
|
1112
|
+
}, 60 * 60 * 1000);
|
|
1113
|
+
// ============================================
|
|
1114
|
+
// START SERVER
|
|
1115
|
+
// ============================================
|
|
1116
|
+
// Start brain worker before starting server
|
|
1117
|
+
brainWorker.start();
|
|
1118
|
+
// Graceful shutdown handler
|
|
1119
|
+
function gracefulShutdown(signal) {
|
|
1120
|
+
console.log(`\n[Server] Received ${signal}, shutting down gracefully...`);
|
|
1121
|
+
// Stop the brain worker
|
|
1122
|
+
brainWorker.stop();
|
|
1123
|
+
// Clear polling intervals
|
|
1124
|
+
clearInterval(eventPollInterval);
|
|
1125
|
+
clearInterval(cleanupInterval);
|
|
1126
|
+
// Close WebSocket connections
|
|
1127
|
+
for (const client of clients) {
|
|
1128
|
+
client.close();
|
|
1129
|
+
}
|
|
1130
|
+
clients.clear();
|
|
1131
|
+
// Close the HTTP server
|
|
1132
|
+
server.close(() => {
|
|
1133
|
+
console.log('[Server] HTTP server closed');
|
|
1134
|
+
// Checkpoint WAL before exit
|
|
1135
|
+
try {
|
|
1136
|
+
checkpointWal();
|
|
1137
|
+
console.log('[Server] WAL checkpointed');
|
|
1138
|
+
}
|
|
1139
|
+
catch (e) {
|
|
1140
|
+
console.error('[Server] Failed to checkpoint WAL:', e);
|
|
1141
|
+
}
|
|
1142
|
+
process.exit(0);
|
|
1143
|
+
});
|
|
1144
|
+
// Force exit after 10 seconds
|
|
1145
|
+
setTimeout(() => {
|
|
1146
|
+
console.error('[Server] Forced exit after timeout');
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}, 10000);
|
|
1149
|
+
}
|
|
1150
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
1151
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
1152
|
+
server.listen(PORT, () => {
|
|
1153
|
+
console.log(`
|
|
1154
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
1155
|
+
║ 🧠 ShieldCortex API Server ║
|
|
1156
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
1157
|
+
║ REST API: http://localhost:${PORT}/api ║
|
|
1158
|
+
║ WebSocket: ws://localhost:${PORT}/ws/events ║
|
|
1159
|
+
║ ║
|
|
1160
|
+
║ Endpoints: ║
|
|
1161
|
+
║ GET /api/health - Health check ║
|
|
1162
|
+
║ GET /api/memories - List memories ║
|
|
1163
|
+
║ GET /api/memories/:id - Get memory ║
|
|
1164
|
+
║ POST /api/memories - Create memory ║
|
|
1165
|
+
║ DEL /api/memories/:id - Delete memory ║
|
|
1166
|
+
║ POST /api/memories/:id/access - Reinforce memory ║
|
|
1167
|
+
║ GET /api/stats - Memory statistics ║
|
|
1168
|
+
║ GET /api/links - Memory relationships ║
|
|
1169
|
+
║ POST /api/consolidate - Trigger consolidation ║
|
|
1170
|
+
║ GET /api/context - Context summary ║
|
|
1171
|
+
║ GET /api/suggestions - Search autocomplete ║
|
|
1172
|
+
║ ║
|
|
1173
|
+
║ Control: ║
|
|
1174
|
+
║ GET /api/control/status - Get pause state & uptime ║
|
|
1175
|
+
║ POST /api/control/pause - Pause memory creation ║
|
|
1176
|
+
║ POST /api/control/resume - Resume memory creation ║
|
|
1177
|
+
║ ║
|
|
1178
|
+
║ Brain Worker: ║
|
|
1179
|
+
║ GET /api/worker/status - Worker status ║
|
|
1180
|
+
║ POST /api/worker/trigger-light - Trigger light tick ║
|
|
1181
|
+
║ POST /api/worker/trigger-medium - Trigger medium tick ║
|
|
1182
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
1183
|
+
`);
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
//# sourceMappingURL=visualization-server.js.map
|