superlocalmemory 2.8.6 → 3.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 +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +1 -1
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1808
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -286
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
- /package/{install.ps1 → scripts/install.ps1} +0 -0
- /package/{install.sh → scripts/install.sh} +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
"""SuperLocalMemory V3 - Memory Routes (MIT License).
|
|
5
|
+
Routes: /api/memories, /api/graph, /api/search, /api/clusters, /api/clusters/{id}
|
|
6
|
+
Uses V3 MemoryEngine for store/recall. Falls back to direct DB for list/graph.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
13
|
+
|
|
14
|
+
from .helpers import (
|
|
15
|
+
get_db_connection, dict_factory, get_active_profile,
|
|
16
|
+
SearchRequest, DB_PATH, MEMORY_DIR,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("superlocalmemory.routes.memories")
|
|
20
|
+
router = APIRouter()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_engine(request: Request):
|
|
24
|
+
"""Get V3 engine from app state, or None."""
|
|
25
|
+
return getattr(request.app.state, "engine", None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _preview(content: str | None) -> str:
|
|
29
|
+
"""Truncate content for preview display."""
|
|
30
|
+
if not content:
|
|
31
|
+
return ""
|
|
32
|
+
return content[:100] + "..." if len(content) > 100 else content
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _has_table(cursor, name: str) -> bool:
|
|
36
|
+
"""Check if a table exists in the database."""
|
|
37
|
+
try:
|
|
38
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (name,))
|
|
39
|
+
return cursor.fetchone() is not None
|
|
40
|
+
except Exception:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _fetch_graph_data(
|
|
45
|
+
cursor, profile: str, use_v3: bool, min_importance: int, max_nodes: int,
|
|
46
|
+
) -> tuple[list, list, list]:
|
|
47
|
+
"""Fetch graph nodes, links, clusters from V3 or V2 schema."""
|
|
48
|
+
if use_v3:
|
|
49
|
+
cursor.execute("""
|
|
50
|
+
SELECT f.fact_id as id, f.content, f.fact_type as category,
|
|
51
|
+
f.confidence as importance, f.session_id as project_name,
|
|
52
|
+
f.created_at
|
|
53
|
+
FROM atomic_facts f WHERE f.profile_id = ? AND f.confidence >= ?
|
|
54
|
+
ORDER BY f.confidence DESC, f.created_at DESC LIMIT ?
|
|
55
|
+
""", (profile, min_importance / 10.0, max_nodes))
|
|
56
|
+
nodes = cursor.fetchall()
|
|
57
|
+
for n in nodes:
|
|
58
|
+
n['entities'] = []
|
|
59
|
+
n['content_preview'] = _preview(n.get('content'))
|
|
60
|
+
ids = [n['id'] for n in nodes]
|
|
61
|
+
links = _fetch_edges_v3(cursor, profile, ids)
|
|
62
|
+
return nodes, links, []
|
|
63
|
+
|
|
64
|
+
# V2 fallback
|
|
65
|
+
try:
|
|
66
|
+
cursor.execute("""
|
|
67
|
+
SELECT m.id, m.content, m.summary, m.category, m.cluster_id,
|
|
68
|
+
m.importance, m.project_name, m.created_at, m.tags, gn.entities
|
|
69
|
+
FROM memories m LEFT JOIN graph_nodes gn ON m.id = gn.memory_id
|
|
70
|
+
WHERE m.importance >= ? AND m.profile = ?
|
|
71
|
+
ORDER BY m.importance DESC, m.updated_at DESC LIMIT ?
|
|
72
|
+
""", (min_importance, profile, max_nodes))
|
|
73
|
+
except Exception:
|
|
74
|
+
cursor.execute("""
|
|
75
|
+
SELECT id, content, summary, category, cluster_id, importance,
|
|
76
|
+
project_name, created_at, tags, NULL as entities
|
|
77
|
+
FROM memories WHERE importance >= ? AND profile = ?
|
|
78
|
+
ORDER BY importance DESC, updated_at DESC LIMIT ?
|
|
79
|
+
""", (min_importance, profile, max_nodes))
|
|
80
|
+
nodes = cursor.fetchall()
|
|
81
|
+
for n in nodes:
|
|
82
|
+
ent = n.get('entities')
|
|
83
|
+
n['entities'] = json.loads(ent) if ent else []
|
|
84
|
+
n['content_preview'] = _preview(n.get('content'))
|
|
85
|
+
ids = [n['id'] for n in nodes]
|
|
86
|
+
links = _fetch_edges_v2(cursor, ids)
|
|
87
|
+
try:
|
|
88
|
+
cursor.execute("""
|
|
89
|
+
SELECT cluster_id, COUNT(*) as size, AVG(importance) as avg_importance
|
|
90
|
+
FROM memories WHERE cluster_id IS NOT NULL AND profile = ?
|
|
91
|
+
GROUP BY cluster_id
|
|
92
|
+
""", (profile,))
|
|
93
|
+
clusters = cursor.fetchall()
|
|
94
|
+
except Exception:
|
|
95
|
+
clusters = []
|
|
96
|
+
return nodes, links, clusters
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _fetch_edges_v3(cursor, profile: str, fact_ids: list) -> list:
|
|
100
|
+
if not fact_ids:
|
|
101
|
+
return []
|
|
102
|
+
ph = ','.join('?' * len(fact_ids))
|
|
103
|
+
try:
|
|
104
|
+
cursor.execute(f"""
|
|
105
|
+
SELECT source_id as source, target_id as target,
|
|
106
|
+
weight, edge_type as relationship_type
|
|
107
|
+
FROM kg_edges WHERE profile_id = ?
|
|
108
|
+
AND source_id IN ({ph}) AND target_id IN ({ph})
|
|
109
|
+
ORDER BY weight DESC
|
|
110
|
+
""", [profile] + fact_ids + fact_ids)
|
|
111
|
+
return cursor.fetchall()
|
|
112
|
+
except Exception:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _fetch_edges_v2(cursor, memory_ids: list) -> list:
|
|
117
|
+
if not memory_ids:
|
|
118
|
+
return []
|
|
119
|
+
ph = ','.join('?' * len(memory_ids))
|
|
120
|
+
try:
|
|
121
|
+
cursor.execute(f"""
|
|
122
|
+
SELECT source_memory_id as source, target_memory_id as target,
|
|
123
|
+
weight, relationship_type, shared_entities
|
|
124
|
+
FROM graph_edges
|
|
125
|
+
WHERE source_memory_id IN ({ph}) AND target_memory_id IN ({ph})
|
|
126
|
+
ORDER BY weight DESC
|
|
127
|
+
""", memory_ids + memory_ids)
|
|
128
|
+
links = cursor.fetchall()
|
|
129
|
+
for lk in links:
|
|
130
|
+
se = lk.get('shared_entities')
|
|
131
|
+
if se:
|
|
132
|
+
try:
|
|
133
|
+
lk['shared_entities'] = json.loads(se)
|
|
134
|
+
except Exception:
|
|
135
|
+
lk['shared_entities'] = []
|
|
136
|
+
return links
|
|
137
|
+
except Exception:
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@router.get("/api/memories")
|
|
142
|
+
async def get_memories(
|
|
143
|
+
request: Request,
|
|
144
|
+
category: Optional[str] = None,
|
|
145
|
+
project_name: Optional[str] = None,
|
|
146
|
+
cluster_id: Optional[int] = None,
|
|
147
|
+
min_importance: Optional[int] = None,
|
|
148
|
+
tags: Optional[str] = None,
|
|
149
|
+
limit: int = Query(50, ge=1, le=200),
|
|
150
|
+
offset: int = Query(0, ge=0),
|
|
151
|
+
):
|
|
152
|
+
"""List memories with optional filtering and pagination."""
|
|
153
|
+
try:
|
|
154
|
+
conn = get_db_connection()
|
|
155
|
+
conn.row_factory = dict_factory
|
|
156
|
+
cursor = conn.cursor()
|
|
157
|
+
active_profile = get_active_profile()
|
|
158
|
+
|
|
159
|
+
use_v3 = _has_table(cursor, 'atomic_facts')
|
|
160
|
+
|
|
161
|
+
if use_v3:
|
|
162
|
+
query = """
|
|
163
|
+
SELECT fact_id as id, content, fact_type as category,
|
|
164
|
+
confidence as importance, access_count,
|
|
165
|
+
created_at, created_at as updated_at,
|
|
166
|
+
session_id as project_name
|
|
167
|
+
FROM atomic_facts WHERE profile_id = ?
|
|
168
|
+
"""
|
|
169
|
+
params = [active_profile]
|
|
170
|
+
count_base = "SELECT COUNT(*) as total FROM atomic_facts WHERE profile_id = ?"
|
|
171
|
+
else:
|
|
172
|
+
query = """
|
|
173
|
+
SELECT id, content, summary, category, project_name, project_path,
|
|
174
|
+
importance, cluster_id, depth, access_count, parent_id,
|
|
175
|
+
created_at, updated_at, last_accessed, tags, memory_type
|
|
176
|
+
FROM memories WHERE profile = ?
|
|
177
|
+
"""
|
|
178
|
+
params = [active_profile]
|
|
179
|
+
count_base = "SELECT COUNT(*) as total FROM memories WHERE profile = ?"
|
|
180
|
+
|
|
181
|
+
count_params = [active_profile]
|
|
182
|
+
|
|
183
|
+
if category:
|
|
184
|
+
if use_v3:
|
|
185
|
+
query += " AND fact_type = ?"
|
|
186
|
+
else:
|
|
187
|
+
query += " AND category = ?"
|
|
188
|
+
params.append(category)
|
|
189
|
+
count_base += " AND category = ?" if not use_v3 else " AND fact_type = ?"
|
|
190
|
+
count_params.append(category)
|
|
191
|
+
if project_name:
|
|
192
|
+
if use_v3:
|
|
193
|
+
query += " AND session_id = ?"
|
|
194
|
+
else:
|
|
195
|
+
query += " AND project_name = ?"
|
|
196
|
+
params.append(project_name)
|
|
197
|
+
count_base += " AND project_name = ?" if not use_v3 else " AND session_id = ?"
|
|
198
|
+
count_params.append(project_name)
|
|
199
|
+
if cluster_id is not None and not use_v3:
|
|
200
|
+
query += " AND cluster_id = ?"
|
|
201
|
+
params.append(cluster_id)
|
|
202
|
+
count_base += " AND cluster_id = ?"
|
|
203
|
+
count_params.append(cluster_id)
|
|
204
|
+
if min_importance:
|
|
205
|
+
if use_v3:
|
|
206
|
+
query += " AND confidence >= ?"
|
|
207
|
+
params.append(min_importance / 10.0)
|
|
208
|
+
else:
|
|
209
|
+
query += " AND importance >= ?"
|
|
210
|
+
params.append(min_importance)
|
|
211
|
+
if tags and not use_v3:
|
|
212
|
+
tag_list = [t.strip() for t in tags.split(',')]
|
|
213
|
+
for tag in tag_list:
|
|
214
|
+
query += " AND tags LIKE ?"
|
|
215
|
+
params.append(f'%{tag}%')
|
|
216
|
+
|
|
217
|
+
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
218
|
+
params.extend([limit, offset])
|
|
219
|
+
|
|
220
|
+
cursor.execute(query, params)
|
|
221
|
+
memories = cursor.fetchall()
|
|
222
|
+
|
|
223
|
+
cursor.execute(count_base, count_params)
|
|
224
|
+
total = cursor.fetchone()['total']
|
|
225
|
+
|
|
226
|
+
conn.close()
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"memories": memories, "total": total,
|
|
230
|
+
"limit": limit, "offset": offset,
|
|
231
|
+
"has_more": (offset + limit) < total,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@router.get("/api/graph")
|
|
239
|
+
async def get_graph(
|
|
240
|
+
request: Request,
|
|
241
|
+
max_nodes: int = Query(100, ge=10, le=500),
|
|
242
|
+
min_importance: int = Query(1, ge=1, le=10),
|
|
243
|
+
):
|
|
244
|
+
"""Get knowledge graph data for D3.js force-directed visualization."""
|
|
245
|
+
try:
|
|
246
|
+
conn = get_db_connection()
|
|
247
|
+
conn.row_factory = dict_factory
|
|
248
|
+
cursor = conn.cursor()
|
|
249
|
+
active_profile = get_active_profile()
|
|
250
|
+
|
|
251
|
+
use_v3 = _has_table(cursor, 'kg_edges')
|
|
252
|
+
|
|
253
|
+
nodes, links, clusters = _fetch_graph_data(
|
|
254
|
+
cursor, active_profile, use_v3, min_importance, max_nodes,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
conn.close()
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"nodes": nodes, "links": links, "clusters": clusters,
|
|
261
|
+
"metadata": {
|
|
262
|
+
"node_count": len(nodes), "edge_count": len(links),
|
|
263
|
+
"cluster_count": len(clusters) if clusters else 0,
|
|
264
|
+
"filters_applied": {"max_nodes": max_nodes, "min_importance": min_importance},
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise HTTPException(status_code=500, detail=f"Graph error: {str(e)}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@router.post("/api/search")
|
|
273
|
+
async def search_memories(request: Request, body: SearchRequest):
|
|
274
|
+
"""Semantic search using V3 engine recall or fallback."""
|
|
275
|
+
try:
|
|
276
|
+
engine = _get_engine(request)
|
|
277
|
+
|
|
278
|
+
if engine:
|
|
279
|
+
response = engine.recall(body.query, limit=body.limit)
|
|
280
|
+
results = []
|
|
281
|
+
for r in response.results:
|
|
282
|
+
score = r.score
|
|
283
|
+
if score < body.min_score:
|
|
284
|
+
continue
|
|
285
|
+
if body.category and getattr(r.fact, 'fact_type', None) != body.category:
|
|
286
|
+
continue
|
|
287
|
+
results.append({
|
|
288
|
+
"id": r.fact.fact_id,
|
|
289
|
+
"content": r.fact.content,
|
|
290
|
+
"score": round(score, 4),
|
|
291
|
+
"confidence": round(r.confidence, 4),
|
|
292
|
+
"trust_score": round(r.trust_score, 4) if r.trust_score else None,
|
|
293
|
+
"channel_scores": r.channel_scores,
|
|
294
|
+
"fact_type": getattr(r.fact, 'fact_type', None),
|
|
295
|
+
"created_at": getattr(r.fact, 'created_at', None),
|
|
296
|
+
})
|
|
297
|
+
if len(results) >= body.limit:
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
"query": body.query, "results": results, "total": len(results),
|
|
302
|
+
"query_type": response.query_type,
|
|
303
|
+
"retrieval_time_ms": response.retrieval_time_ms,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Fallback: direct DB search (no V3 engine)
|
|
307
|
+
conn = get_db_connection()
|
|
308
|
+
conn.row_factory = dict_factory
|
|
309
|
+
cursor = conn.cursor()
|
|
310
|
+
active_profile = get_active_profile()
|
|
311
|
+
cursor.execute("""
|
|
312
|
+
SELECT fact_id as id, content, confidence as score,
|
|
313
|
+
fact_type as category, created_at
|
|
314
|
+
FROM atomic_facts
|
|
315
|
+
WHERE profile_id = ? AND content LIKE ?
|
|
316
|
+
ORDER BY confidence DESC LIMIT ?
|
|
317
|
+
""", (active_profile, f'%{body.query}%', body.limit))
|
|
318
|
+
results = cursor.fetchall()
|
|
319
|
+
conn.close()
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"query": body.query, "results": results, "total": len(results),
|
|
323
|
+
"query_type": "text_search", "retrieval_time_ms": 0,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
raise HTTPException(status_code=500, detail=f"Search error: {str(e)}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@router.get("/api/clusters")
|
|
331
|
+
async def get_clusters(request: Request):
|
|
332
|
+
"""Get cluster information with member counts and statistics."""
|
|
333
|
+
try:
|
|
334
|
+
conn = get_db_connection()
|
|
335
|
+
conn.row_factory = dict_factory
|
|
336
|
+
cursor = conn.cursor()
|
|
337
|
+
profile = get_active_profile()
|
|
338
|
+
unclustered = 0
|
|
339
|
+
|
|
340
|
+
if _has_table(cursor, 'scene_facts'):
|
|
341
|
+
cursor.execute("""
|
|
342
|
+
SELECT s.scene_id as cluster_id, COUNT(sf.fact_id) as member_count,
|
|
343
|
+
s.summary, s.created_at as first_memory
|
|
344
|
+
FROM scenes s JOIN scene_facts sf ON s.scene_id = sf.scene_id
|
|
345
|
+
WHERE s.profile_id = ? GROUP BY s.scene_id ORDER BY member_count DESC
|
|
346
|
+
""", (profile,))
|
|
347
|
+
clusters = [dict(r, top_entities=[]) for r in cursor.fetchall()]
|
|
348
|
+
else:
|
|
349
|
+
try:
|
|
350
|
+
cursor.execute("""
|
|
351
|
+
SELECT cluster_id, COUNT(*) as member_count,
|
|
352
|
+
AVG(importance) as avg_importance,
|
|
353
|
+
GROUP_CONCAT(DISTINCT category) as categories
|
|
354
|
+
FROM memories WHERE cluster_id IS NOT NULL AND profile = ?
|
|
355
|
+
GROUP BY cluster_id ORDER BY member_count DESC
|
|
356
|
+
""", (profile,))
|
|
357
|
+
clusters = [dict(r, top_entities=[]) for r in cursor.fetchall()]
|
|
358
|
+
except Exception:
|
|
359
|
+
clusters = []
|
|
360
|
+
cursor.execute("SELECT COUNT(*) as c FROM memories WHERE cluster_id IS NULL AND profile = ?", (profile,))
|
|
361
|
+
unclustered = cursor.fetchone()['c']
|
|
362
|
+
|
|
363
|
+
conn.close()
|
|
364
|
+
return {"clusters": clusters, "total_clusters": len(clusters), "unclustered_count": unclustered}
|
|
365
|
+
except Exception as e:
|
|
366
|
+
raise HTTPException(status_code=500, detail=f"Cluster error: {str(e)}")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@router.get("/api/clusters/{cluster_id}")
|
|
370
|
+
async def get_cluster_detail(request: Request, cluster_id: int, limit: int = Query(50, ge=1, le=200)):
|
|
371
|
+
"""Get detailed view of a specific cluster."""
|
|
372
|
+
try:
|
|
373
|
+
conn = get_db_connection()
|
|
374
|
+
conn.row_factory = dict_factory
|
|
375
|
+
cursor = conn.cursor()
|
|
376
|
+
profile = get_active_profile()
|
|
377
|
+
|
|
378
|
+
if _has_table(cursor, 'scene_facts'):
|
|
379
|
+
cursor.execute("""
|
|
380
|
+
SELECT f.fact_id as id, f.content, f.fact_type as category,
|
|
381
|
+
f.confidence as importance, f.created_at
|
|
382
|
+
FROM atomic_facts f JOIN scene_facts sf ON f.fact_id = sf.fact_id
|
|
383
|
+
WHERE sf.scene_id = ? AND f.profile_id = ? ORDER BY f.confidence DESC LIMIT ?
|
|
384
|
+
""", (str(cluster_id), profile, limit))
|
|
385
|
+
else:
|
|
386
|
+
cursor.execute("""
|
|
387
|
+
SELECT id, content, summary, category, project_name, importance, created_at, tags
|
|
388
|
+
FROM memories WHERE cluster_id = ? AND profile = ?
|
|
389
|
+
ORDER BY importance DESC, created_at DESC LIMIT ?
|
|
390
|
+
""", (cluster_id, profile, limit))
|
|
391
|
+
members = cursor.fetchall()
|
|
392
|
+
conn.close()
|
|
393
|
+
if not members:
|
|
394
|
+
raise HTTPException(status_code=404, detail="Cluster not found")
|
|
395
|
+
return {"cluster_info": {"cluster_id": cluster_id, "total_members": len(members)}, "members": members, "connections": []}
|
|
396
|
+
except HTTPException:
|
|
397
|
+
raise
|
|
398
|
+
except Exception as e:
|
|
399
|
+
raise HTTPException(status_code=500, detail=f"Cluster detail error: {str(e)}")
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
"""SuperLocalMemory V3 - Profile Routes
|
|
5
|
+
- MIT License
|
|
6
|
+
|
|
7
|
+
Routes: /api/profiles, /api/profiles/{name}/switch,
|
|
8
|
+
/api/profiles/create, DELETE /api/profiles/{name}
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, HTTPException
|
|
15
|
+
|
|
16
|
+
from .helpers import (
|
|
17
|
+
get_db_connection, get_active_profile, validate_profile_name,
|
|
18
|
+
ProfileSwitch, MEMORY_DIR, DB_PATH,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("superlocalmemory.routes.profiles")
|
|
22
|
+
router = APIRouter()
|
|
23
|
+
|
|
24
|
+
# WebSocket manager reference (set by ui_server.py at startup)
|
|
25
|
+
ws_manager = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_profiles_config() -> dict:
|
|
29
|
+
"""Load profiles.json config."""
|
|
30
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
31
|
+
if config_file.exists():
|
|
32
|
+
try:
|
|
33
|
+
with open(config_file, 'r') as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
except (json.JSONDecodeError, IOError):
|
|
36
|
+
pass
|
|
37
|
+
return {
|
|
38
|
+
'profiles': {'default': {'name': 'default', 'description': 'Default memory profile'}},
|
|
39
|
+
'active_profile': 'default',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _save_profiles_config(config: dict) -> None:
|
|
44
|
+
"""Save profiles.json config."""
|
|
45
|
+
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
47
|
+
with open(config_file, 'w') as f:
|
|
48
|
+
json.dump(config, f, indent=2)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_memory_count(profile: str) -> int:
|
|
52
|
+
"""Get memory count for a profile (V3 atomic_facts or V2 memories)."""
|
|
53
|
+
try:
|
|
54
|
+
conn = get_db_connection()
|
|
55
|
+
cursor = conn.cursor()
|
|
56
|
+
# Try V3 table first
|
|
57
|
+
try:
|
|
58
|
+
cursor.execute(
|
|
59
|
+
"SELECT COUNT(*) FROM atomic_facts WHERE profile_id = ?", (profile,),
|
|
60
|
+
)
|
|
61
|
+
count = cursor.fetchone()[0]
|
|
62
|
+
except Exception:
|
|
63
|
+
cursor.execute(
|
|
64
|
+
"SELECT COUNT(*) FROM memories WHERE profile = ?", (profile,),
|
|
65
|
+
)
|
|
66
|
+
count = cursor.fetchone()[0]
|
|
67
|
+
conn.close()
|
|
68
|
+
return count
|
|
69
|
+
except Exception:
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.get("/api/profiles")
|
|
74
|
+
async def list_profiles():
|
|
75
|
+
"""List available memory profiles."""
|
|
76
|
+
try:
|
|
77
|
+
config = _load_profiles_config()
|
|
78
|
+
active = config.get('active_profile', 'default')
|
|
79
|
+
profiles = []
|
|
80
|
+
|
|
81
|
+
for name, info in config.get('profiles', {}).items():
|
|
82
|
+
count = _get_memory_count(name)
|
|
83
|
+
profiles.append({
|
|
84
|
+
"name": name,
|
|
85
|
+
"description": info.get('description', ''),
|
|
86
|
+
"memory_count": count,
|
|
87
|
+
"created_at": info.get('created_at', ''),
|
|
88
|
+
"last_used": info.get('last_used', ''),
|
|
89
|
+
"is_active": name == active,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"profiles": profiles,
|
|
94
|
+
"active_profile": active,
|
|
95
|
+
"total_profiles": len(profiles),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise HTTPException(status_code=500, detail=f"Profile list error: {str(e)}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.post("/api/profiles/{name}/switch")
|
|
103
|
+
async def switch_profile(name: str):
|
|
104
|
+
"""Switch active memory profile."""
|
|
105
|
+
try:
|
|
106
|
+
if not validate_profile_name(name):
|
|
107
|
+
raise HTTPException(status_code=400, detail="Invalid profile name.")
|
|
108
|
+
|
|
109
|
+
config = _load_profiles_config()
|
|
110
|
+
|
|
111
|
+
if name not in config.get('profiles', {}):
|
|
112
|
+
available = ', '.join(config.get('profiles', {}).keys())
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
status_code=404,
|
|
115
|
+
detail=f"Profile '{name}' not found. Available: {available}",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
previous = config.get('active_profile', 'default')
|
|
119
|
+
config['active_profile'] = name
|
|
120
|
+
config['profiles'][name]['last_used'] = datetime.now().isoformat()
|
|
121
|
+
_save_profiles_config(config)
|
|
122
|
+
|
|
123
|
+
count = _get_memory_count(name)
|
|
124
|
+
|
|
125
|
+
if ws_manager:
|
|
126
|
+
await ws_manager.broadcast({
|
|
127
|
+
"type": "profile_switched", "profile": name,
|
|
128
|
+
"previous": previous, "memory_count": count,
|
|
129
|
+
"timestamp": datetime.now().isoformat(),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"success": True, "active_profile": name,
|
|
134
|
+
"previous_profile": previous, "memory_count": count,
|
|
135
|
+
"message": f"Switched to profile '{name}' ({count} memories).",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
except HTTPException:
|
|
139
|
+
raise
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise HTTPException(status_code=500, detail=f"Profile switch error: {str(e)}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@router.post("/api/profiles/create")
|
|
145
|
+
async def create_profile(body: ProfileSwitch):
|
|
146
|
+
"""Create a new memory profile."""
|
|
147
|
+
try:
|
|
148
|
+
name = body.profile_name
|
|
149
|
+
if not validate_profile_name(name):
|
|
150
|
+
raise HTTPException(status_code=400, detail="Invalid profile name")
|
|
151
|
+
|
|
152
|
+
config = _load_profiles_config()
|
|
153
|
+
|
|
154
|
+
if name in config.get('profiles', {}):
|
|
155
|
+
raise HTTPException(status_code=409, detail=f"Profile '{name}' already exists")
|
|
156
|
+
|
|
157
|
+
config['profiles'][name] = {
|
|
158
|
+
'name': name, 'description': f'Memory profile: {name}',
|
|
159
|
+
'created_at': datetime.now().isoformat(), 'last_used': None,
|
|
160
|
+
}
|
|
161
|
+
_save_profiles_config(config)
|
|
162
|
+
|
|
163
|
+
return {"success": True, "profile": name, "message": f"Profile '{name}' created"}
|
|
164
|
+
|
|
165
|
+
except HTTPException:
|
|
166
|
+
raise
|
|
167
|
+
except Exception as e:
|
|
168
|
+
raise HTTPException(status_code=500, detail=f"Profile create error: {str(e)}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.delete("/api/profiles/{name}")
|
|
172
|
+
async def delete_profile(name: str):
|
|
173
|
+
"""Delete a profile. Moves its memories to 'default'."""
|
|
174
|
+
try:
|
|
175
|
+
if name == 'default':
|
|
176
|
+
raise HTTPException(status_code=400, detail="Cannot delete 'default' profile")
|
|
177
|
+
|
|
178
|
+
config = _load_profiles_config()
|
|
179
|
+
|
|
180
|
+
if name not in config.get('profiles', {}):
|
|
181
|
+
raise HTTPException(status_code=404, detail=f"Profile '{name}' not found")
|
|
182
|
+
if config.get('active_profile') == name:
|
|
183
|
+
raise HTTPException(status_code=400, detail="Cannot delete active profile.")
|
|
184
|
+
|
|
185
|
+
conn = get_db_connection()
|
|
186
|
+
cursor = conn.cursor()
|
|
187
|
+
# Move memories to default (try V3 first, then V2)
|
|
188
|
+
moved = 0
|
|
189
|
+
try:
|
|
190
|
+
cursor.execute(
|
|
191
|
+
"UPDATE atomic_facts SET profile_id = 'default' WHERE profile_id = ?",
|
|
192
|
+
(name,),
|
|
193
|
+
)
|
|
194
|
+
moved = cursor.rowcount
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
try:
|
|
198
|
+
cursor.execute(
|
|
199
|
+
"UPDATE memories SET profile = 'default' WHERE profile = ?",
|
|
200
|
+
(name,),
|
|
201
|
+
)
|
|
202
|
+
moved += cursor.rowcount
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
conn.commit()
|
|
206
|
+
conn.close()
|
|
207
|
+
|
|
208
|
+
del config['profiles'][name]
|
|
209
|
+
_save_profiles_config(config)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"success": True,
|
|
213
|
+
"message": f"Profile '{name}' deleted. {moved} memories moved to 'default'.",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
except HTTPException:
|
|
217
|
+
raise
|
|
218
|
+
except Exception as e:
|
|
219
|
+
raise HTTPException(status_code=500, detail=f"Profile delete error: {str(e)}")
|