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,65 @@
|
|
|
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
|
+
|
|
5
|
+
"""Engine lifecycle hooks -- pre/post operation dispatch.
|
|
6
|
+
|
|
7
|
+
Pre-hooks are synchronous and can reject operations (raise exceptions).
|
|
8
|
+
Post-hooks are fire-and-forget -- errors are logged, never propagated.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HookRegistry:
|
|
20
|
+
"""Registry for engine lifecycle hooks.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
registry = HookRegistry()
|
|
24
|
+
registry.register_pre("store", abac_check)
|
|
25
|
+
registry.register_pre("store", trust_gate_check)
|
|
26
|
+
registry.register_post("store", audit_log)
|
|
27
|
+
registry.register_post("store", event_bus_publish)
|
|
28
|
+
|
|
29
|
+
# In engine.store():
|
|
30
|
+
registry.run_pre("store", context) # raises if any pre-hook fails
|
|
31
|
+
# ... core operation ...
|
|
32
|
+
registry.run_post("store", context) # never raises
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._pre: dict[str, list[Callable]] = {}
|
|
37
|
+
self._post: dict[str, list[Callable]] = {}
|
|
38
|
+
|
|
39
|
+
def register_pre(self, operation: str, hook: Callable[[dict[str, Any]], None]) -> None:
|
|
40
|
+
"""Register a pre-operation hook. Hook receives context dict.
|
|
41
|
+
Hook can raise to reject the operation."""
|
|
42
|
+
self._pre.setdefault(operation, []).append(hook)
|
|
43
|
+
|
|
44
|
+
def register_post(self, operation: str, hook: Callable[[dict[str, Any]], None]) -> None:
|
|
45
|
+
"""Register a post-operation hook. Hook receives context dict.
|
|
46
|
+
Errors are logged, never propagated."""
|
|
47
|
+
self._post.setdefault(operation, []).append(hook)
|
|
48
|
+
|
|
49
|
+
def run_pre(self, operation: str, context: dict[str, Any]) -> None:
|
|
50
|
+
"""Run all pre-hooks for an operation. Raises on first failure."""
|
|
51
|
+
for hook in self._pre.get(operation, []):
|
|
52
|
+
hook(context) # Let exceptions propagate -- this is intentional
|
|
53
|
+
|
|
54
|
+
def run_post(self, operation: str, context: dict[str, Any]) -> None:
|
|
55
|
+
"""Run all post-hooks for an operation. Errors logged, never raised."""
|
|
56
|
+
for hook in self._post.get(operation, []):
|
|
57
|
+
try:
|
|
58
|
+
hook(context)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
logger.debug("Post-hook error (%s): %s", operation, exc)
|
|
61
|
+
|
|
62
|
+
def clear(self) -> None:
|
|
63
|
+
"""Remove all hooks. Useful for testing."""
|
|
64
|
+
self._pre.clear()
|
|
65
|
+
self._post.clear()
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
|
|
5
|
+
"""SuperLocalMemory V3 — Background Math Maintenance.
|
|
6
|
+
|
|
7
|
+
Periodic batch processing for mathematical layers:
|
|
8
|
+
1. Langevin batch_step on all active facts (self-organization)
|
|
9
|
+
2. Sheaf batch consistency check on recent facts
|
|
10
|
+
3. Fisher adaptive temperature recalculation
|
|
11
|
+
|
|
12
|
+
Frequency: every 6-24h or after 100 stores.
|
|
13
|
+
~100 Langevin steps to stationarity.
|
|
14
|
+
|
|
15
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
16
|
+
License: MIT
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from datetime import UTC, datetime, timedelta
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from superlocalmemory.core.config import SLMConfig
|
|
26
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_maintenance(
|
|
32
|
+
db: DatabaseManager,
|
|
33
|
+
config: SLMConfig,
|
|
34
|
+
profile_id: str = "default",
|
|
35
|
+
) -> dict[str, int]:
|
|
36
|
+
"""Run background maintenance on mathematical layers.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
db: Database manager.
|
|
40
|
+
config: Full SLM configuration.
|
|
41
|
+
profile_id: Scope to this profile.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dict of counts: langevin_updated, sheaf_checked, etc.
|
|
45
|
+
"""
|
|
46
|
+
counts: dict[str, int] = {
|
|
47
|
+
"langevin_updated": 0,
|
|
48
|
+
"fisher_coupled": 0,
|
|
49
|
+
"sheaf_checked": 0,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
facts = db.get_all_facts(profile_id)
|
|
53
|
+
if not facts:
|
|
54
|
+
return counts
|
|
55
|
+
|
|
56
|
+
# 1. Langevin batch step
|
|
57
|
+
if config.math.langevin_persist_positions:
|
|
58
|
+
try:
|
|
59
|
+
from superlocalmemory.math.langevin import LangevinDynamics
|
|
60
|
+
|
|
61
|
+
ld = LangevinDynamics(
|
|
62
|
+
dim=8,
|
|
63
|
+
dt=config.math.langevin_dt,
|
|
64
|
+
temperature=config.math.langevin_temperature,
|
|
65
|
+
)
|
|
66
|
+
fact_dicts = []
|
|
67
|
+
for f in facts:
|
|
68
|
+
if f.langevin_position is None:
|
|
69
|
+
continue
|
|
70
|
+
created = datetime.fromisoformat(
|
|
71
|
+
f.created_at.replace("Z", "+00:00")
|
|
72
|
+
) if f.created_at else datetime.now(UTC)
|
|
73
|
+
age_days = max(
|
|
74
|
+
0.0,
|
|
75
|
+
(datetime.now(UTC) - created).total_seconds() / 86400.0,
|
|
76
|
+
)
|
|
77
|
+
fact_dicts.append({
|
|
78
|
+
"fact_id": f.fact_id,
|
|
79
|
+
"position": f.langevin_position,
|
|
80
|
+
"access_count": f.access_count,
|
|
81
|
+
"age_days": age_days,
|
|
82
|
+
"importance": f.importance,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if fact_dicts:
|
|
86
|
+
results = ld.batch_step(fact_dicts)
|
|
87
|
+
for r in results:
|
|
88
|
+
db.update_fact(r["fact_id"], {
|
|
89
|
+
"langevin_position": r["position"],
|
|
90
|
+
"lifecycle": r["lifecycle"],
|
|
91
|
+
})
|
|
92
|
+
counts["langevin_updated"] = len(results)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
logger.warning("Langevin maintenance failed: %s", exc)
|
|
95
|
+
|
|
96
|
+
# 1b. Fisher-Langevin coupling: modulate temperature per-fact
|
|
97
|
+
# High Fisher confidence (low variance) -> low temperature -> memory stabilizes
|
|
98
|
+
# Low Fisher confidence (high variance) -> high temperature -> memory fades
|
|
99
|
+
if config.math.langevin_persist_positions and counts["langevin_updated"] > 0:
|
|
100
|
+
try:
|
|
101
|
+
from superlocalmemory.dynamics.fisher_langevin_coupling import (
|
|
102
|
+
FisherLangevinCoupling,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
coupling = FisherLangevinCoupling(
|
|
106
|
+
base_temperature=config.math.langevin_temperature,
|
|
107
|
+
)
|
|
108
|
+
coupled_count = 0
|
|
109
|
+
|
|
110
|
+
for f in facts:
|
|
111
|
+
if f.langevin_position is None or f.fisher_variance is None:
|
|
112
|
+
continue
|
|
113
|
+
eff_temp = coupling.get_effective_temperature(
|
|
114
|
+
f.fisher_variance, f.access_count,
|
|
115
|
+
)
|
|
116
|
+
# Re-run Langevin step with Fisher-coupled temperature
|
|
117
|
+
# only if it differs meaningfully from the base temperature
|
|
118
|
+
if abs(eff_temp - config.math.langevin_temperature) > 0.01:
|
|
119
|
+
from superlocalmemory.math.langevin import LangevinDynamics
|
|
120
|
+
|
|
121
|
+
coupled_ld = LangevinDynamics(
|
|
122
|
+
dim=8,
|
|
123
|
+
dt=config.math.langevin_dt,
|
|
124
|
+
temperature=eff_temp,
|
|
125
|
+
)
|
|
126
|
+
created = datetime.fromisoformat(
|
|
127
|
+
f.created_at.replace("Z", "+00:00")
|
|
128
|
+
) if f.created_at else datetime.now(UTC)
|
|
129
|
+
age_days = max(
|
|
130
|
+
0.0,
|
|
131
|
+
(datetime.now(UTC) - created).total_seconds() / 86400.0,
|
|
132
|
+
)
|
|
133
|
+
new_pos, weight = coupled_ld.step(
|
|
134
|
+
position=f.langevin_position,
|
|
135
|
+
access_count=f.access_count,
|
|
136
|
+
age_days=age_days,
|
|
137
|
+
importance=f.importance,
|
|
138
|
+
)
|
|
139
|
+
lifecycle = coupled_ld.get_lifecycle_state(weight).value
|
|
140
|
+
db.update_fact(f.fact_id, {
|
|
141
|
+
"langevin_position": new_pos,
|
|
142
|
+
"lifecycle": lifecycle,
|
|
143
|
+
})
|
|
144
|
+
coupled_count += 1
|
|
145
|
+
|
|
146
|
+
counts["fisher_coupled"] = coupled_count
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
logger.warning("Fisher-Langevin coupling failed: %s", exc)
|
|
149
|
+
|
|
150
|
+
# 2. Sheaf batch consistency on recent facts (last 24h)
|
|
151
|
+
if config.math.sheaf_at_encoding:
|
|
152
|
+
try:
|
|
153
|
+
from superlocalmemory.math.sheaf import SheafConsistencyChecker
|
|
154
|
+
|
|
155
|
+
checker = SheafConsistencyChecker(
|
|
156
|
+
db, config.math.sheaf_contradiction_threshold,
|
|
157
|
+
)
|
|
158
|
+
cutoff = (datetime.now(UTC) - timedelta(hours=24)).isoformat()
|
|
159
|
+
recent = [f for f in facts if f.created_at and f.created_at >= cutoff]
|
|
160
|
+
for f in recent:
|
|
161
|
+
if f.embedding and f.canonical_entities:
|
|
162
|
+
checker.check_consistency(f, profile_id)
|
|
163
|
+
counts["sheaf_checked"] += 1
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
logger.warning("Sheaf maintenance failed: %s", exc)
|
|
166
|
+
|
|
167
|
+
logger.info(
|
|
168
|
+
"Maintenance complete: %d Langevin, %d Fisher-coupled, %d Sheaf",
|
|
169
|
+
counts["langevin_updated"], counts["fisher_coupled"],
|
|
170
|
+
counts["sheaf_checked"],
|
|
171
|
+
)
|
|
172
|
+
return counts
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
|
|
5
|
+
"""SuperLocalMemory V3 — Mode System.
|
|
6
|
+
|
|
7
|
+
Three operating modes with clear capability boundaries.
|
|
8
|
+
Mode A: EU AI Act FULL compliance (zero LLM).
|
|
9
|
+
Mode B: EU AI Act FULL (local LLM only).
|
|
10
|
+
Mode C: UNRESTRICTED — best models, full power, 90%+ target.
|
|
11
|
+
|
|
12
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from superlocalmemory.storage.models import Mode
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ModeCapabilities:
|
|
24
|
+
"""What each mode can and cannot do."""
|
|
25
|
+
|
|
26
|
+
mode: Mode
|
|
27
|
+
|
|
28
|
+
# Encoding capabilities
|
|
29
|
+
llm_fact_extraction: bool # Can use LLM for fact extraction?
|
|
30
|
+
llm_entity_resolution: bool # Can use LLM for entity disambiguation?
|
|
31
|
+
llm_type_classification: bool # Can use LLM for fact type routing?
|
|
32
|
+
llm_importance_scoring: bool # Can use LLM for importance assessment?
|
|
33
|
+
|
|
34
|
+
# Retrieval capabilities
|
|
35
|
+
agentic_retrieval: bool # Can do multi-round LLM-guided retrieval?
|
|
36
|
+
llm_answer_generation: bool # Can LLM generate answers from context?
|
|
37
|
+
cloud_reranker: bool # Can use Cohere / cloud reranker?
|
|
38
|
+
|
|
39
|
+
# Embedding capabilities
|
|
40
|
+
cloud_embeddings: bool # Can use cloud embedding API?
|
|
41
|
+
embedding_dimension: int # Expected embedding dimension
|
|
42
|
+
|
|
43
|
+
# Compliance
|
|
44
|
+
eu_ai_act_compliant: bool # Full EU AI Act compliance?
|
|
45
|
+
data_stays_local: bool # Does ALL data stay on device?
|
|
46
|
+
|
|
47
|
+
# Description
|
|
48
|
+
description: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Mode Definitions
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
MODE_A = ModeCapabilities(
|
|
56
|
+
mode=Mode.A,
|
|
57
|
+
llm_fact_extraction=False,
|
|
58
|
+
llm_entity_resolution=False,
|
|
59
|
+
llm_type_classification=False,
|
|
60
|
+
llm_importance_scoring=False,
|
|
61
|
+
agentic_retrieval=False,
|
|
62
|
+
llm_answer_generation=False,
|
|
63
|
+
cloud_reranker=False,
|
|
64
|
+
cloud_embeddings=False,
|
|
65
|
+
embedding_dimension=768,
|
|
66
|
+
eu_ai_act_compliant=True,
|
|
67
|
+
data_stays_local=True,
|
|
68
|
+
description=(
|
|
69
|
+
"Local Guardian — Zero LLM, zero cloud. "
|
|
70
|
+
"Uses nomic-embed-text-v1.5 encoder (768d, 8K context) for embeddings. "
|
|
71
|
+
"spaCy + rules for extraction. Cross-encoder for reranking. "
|
|
72
|
+
"Full EU AI Act compliance. Target: 65%+"
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
MODE_B = ModeCapabilities(
|
|
77
|
+
mode=Mode.B,
|
|
78
|
+
llm_fact_extraction=True,
|
|
79
|
+
llm_entity_resolution=True,
|
|
80
|
+
llm_type_classification=True,
|
|
81
|
+
llm_importance_scoring=True,
|
|
82
|
+
agentic_retrieval=False,
|
|
83
|
+
llm_answer_generation=True,
|
|
84
|
+
cloud_reranker=False,
|
|
85
|
+
cloud_embeddings=False,
|
|
86
|
+
embedding_dimension=768,
|
|
87
|
+
eu_ai_act_compliant=True,
|
|
88
|
+
data_stays_local=True,
|
|
89
|
+
description=(
|
|
90
|
+
"Smart Local — Local Ollama LLM (Phi-3, Llama 3.2). "
|
|
91
|
+
"LLM-quality extraction and classification, fully local. "
|
|
92
|
+
"No cloud, no data export. EU AI Act compliant. Target: 75-80%"
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
MODE_C = ModeCapabilities(
|
|
97
|
+
mode=Mode.C,
|
|
98
|
+
llm_fact_extraction=True,
|
|
99
|
+
llm_entity_resolution=True,
|
|
100
|
+
llm_type_classification=True,
|
|
101
|
+
llm_importance_scoring=True,
|
|
102
|
+
agentic_retrieval=True,
|
|
103
|
+
llm_answer_generation=True,
|
|
104
|
+
cloud_reranker=True,
|
|
105
|
+
cloud_embeddings=True,
|
|
106
|
+
embedding_dimension=3072,
|
|
107
|
+
eu_ai_act_compliant=False,
|
|
108
|
+
data_stays_local=False,
|
|
109
|
+
description=(
|
|
110
|
+
"FULL POWER — UNRESTRICTED. Best embeddings (text-embedding-3-large, 3072-dim). "
|
|
111
|
+
"Best LLMs (GPT-5.2, Claude Opus). Agentic multi-round retrieval. "
|
|
112
|
+
"Cohere reranker option. No EU restriction. Target: 90%+"
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_capabilities(mode: Mode) -> ModeCapabilities:
|
|
118
|
+
"""Get capability matrix for a mode."""
|
|
119
|
+
_map = {Mode.A: MODE_A, Mode.B: MODE_B, Mode.C: MODE_C}
|
|
120
|
+
return _map[mode]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_mode_config(mode: Mode, *, has_ollama: bool = False, has_cloud_llm: bool = False) -> list[str]:
|
|
124
|
+
"""Validate that required services are available for the chosen mode.
|
|
125
|
+
|
|
126
|
+
Returns list of warnings/errors. Empty list = all good.
|
|
127
|
+
"""
|
|
128
|
+
issues: list[str] = []
|
|
129
|
+
caps = get_capabilities(mode)
|
|
130
|
+
|
|
131
|
+
if caps.llm_fact_extraction and mode == Mode.B and not has_ollama:
|
|
132
|
+
issues.append("Mode B requires Ollama but it is not available. Falling back to Mode A extraction.")
|
|
133
|
+
|
|
134
|
+
if caps.cloud_embeddings and not has_cloud_llm:
|
|
135
|
+
issues.append("Mode C cloud embeddings configured but no API endpoint provided.")
|
|
136
|
+
|
|
137
|
+
if caps.agentic_retrieval and not has_cloud_llm:
|
|
138
|
+
issues.append("Mode C agentic retrieval requires cloud LLM but none configured.")
|
|
139
|
+
|
|
140
|
+
return issues
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
|
|
5
|
+
"""SuperLocalMemory V3 — Profile Management.
|
|
6
|
+
|
|
7
|
+
First-class profile isolation. Every memory, fact, entity, and learning
|
|
8
|
+
record is scoped by profile_id. Profiles are persisted in profiles.json
|
|
9
|
+
with atomic writes and instant switching (config-only, zero data movement).
|
|
10
|
+
|
|
11
|
+
Ported from V2.8 columnar profile isolation pattern.
|
|
12
|
+
|
|
13
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import tempfile
|
|
23
|
+
import uuid
|
|
24
|
+
from dataclasses import asdict
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Callable
|
|
28
|
+
|
|
29
|
+
from superlocalmemory.core.config import DEFAULT_PROFILES_FILE
|
|
30
|
+
from superlocalmemory.storage.models import Mode, Profile
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_VALID_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,49}$")
|
|
35
|
+
_MAX_NAME_LEN = 50
|
|
36
|
+
_RESERVED = "default"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _validate_name(name: str) -> None:
|
|
40
|
+
"""Alphanumeric + dash + underscore, 1-50 chars, starts alphanumeric."""
|
|
41
|
+
if not name or len(name) > _MAX_NAME_LEN:
|
|
42
|
+
raise ValueError(f"Profile name must be 1-{_MAX_NAME_LEN} chars, got {len(name)}.")
|
|
43
|
+
if not _VALID_NAME_RE.match(name):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Invalid profile name '{name}'. "
|
|
46
|
+
"Must start with alphanumeric, then alphanumeric / dash / underscore."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _now() -> str:
|
|
51
|
+
return datetime.now(timezone.utc).isoformat()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _pid() -> str:
|
|
55
|
+
return uuid.uuid4().hex[:16]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _evolve(prof: Profile, **overrides: Any) -> Profile:
|
|
59
|
+
"""Return a new Profile with selected fields replaced (frozen dataclass)."""
|
|
60
|
+
base = asdict(prof)
|
|
61
|
+
base.update(overrides)
|
|
62
|
+
base["mode"] = Mode(base["mode"]) if isinstance(base["mode"], str) else base["mode"]
|
|
63
|
+
return Profile(**base)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ProfileManager:
|
|
67
|
+
"""Thread-safe profile manager with atomic JSON persistence.
|
|
68
|
+
|
|
69
|
+
Stores profile metadata in ``profiles.json`` inside *base_dir*.
|
|
70
|
+
Profiles are isolated at the DB column level (WHERE profile_id = ?).
|
|
71
|
+
Switching is instant — only the active pointer changes.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, base_dir: Path) -> None:
|
|
75
|
+
self._base_dir = base_dir
|
|
76
|
+
self._base_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
self._path = self._base_dir / DEFAULT_PROFILES_FILE
|
|
78
|
+
self._profiles: dict[str, Profile] = {}
|
|
79
|
+
self._active_name: str = _RESERVED
|
|
80
|
+
self._on_switch: Callable[[Profile], None] | None = None
|
|
81
|
+
self._load()
|
|
82
|
+
|
|
83
|
+
# -- Persistence (atomic write via tempfile + rename) ------------------
|
|
84
|
+
|
|
85
|
+
def _load(self) -> None:
|
|
86
|
+
"""Load profiles.json or bootstrap with the default profile."""
|
|
87
|
+
if self._path.exists():
|
|
88
|
+
raw = json.loads(self._path.read_text(encoding="utf-8"))
|
|
89
|
+
self._active_name = raw.get("active", _RESERVED)
|
|
90
|
+
for entry in raw.get("profiles", []):
|
|
91
|
+
mode = Mode(entry["mode"]) if "mode" in entry else Mode.A
|
|
92
|
+
prof = Profile(
|
|
93
|
+
profile_id=entry["profile_id"],
|
|
94
|
+
name=entry["name"],
|
|
95
|
+
description=entry.get("description", ""),
|
|
96
|
+
personality=entry.get("personality", ""),
|
|
97
|
+
mode=mode,
|
|
98
|
+
created_at=entry.get("created_at", _now()),
|
|
99
|
+
last_used=entry.get("last_used"),
|
|
100
|
+
config=entry.get("config", {}),
|
|
101
|
+
)
|
|
102
|
+
self._profiles[prof.name] = prof
|
|
103
|
+
if _RESERVED not in self._profiles:
|
|
104
|
+
self._profiles[_RESERVED] = Profile(
|
|
105
|
+
profile_id=_pid(), name=_RESERVED,
|
|
106
|
+
description="Default memory profile", mode=Mode.A,
|
|
107
|
+
created_at=_now(),
|
|
108
|
+
)
|
|
109
|
+
self._save()
|
|
110
|
+
|
|
111
|
+
def _save(self) -> None:
|
|
112
|
+
"""Atomic write: temp file in same dir, then rename."""
|
|
113
|
+
data = json.dumps(
|
|
114
|
+
{"active": self._active_name,
|
|
115
|
+
"profiles": [asdict(p) for p in self._profiles.values()]},
|
|
116
|
+
indent=2, ensure_ascii=False,
|
|
117
|
+
)
|
|
118
|
+
fd, tmp = tempfile.mkstemp(dir=str(self._base_dir), suffix=".tmp")
|
|
119
|
+
try:
|
|
120
|
+
Path(tmp).write_text(data, encoding="utf-8")
|
|
121
|
+
Path(tmp).replace(self._path)
|
|
122
|
+
finally:
|
|
123
|
+
try:
|
|
124
|
+
os.close(fd)
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
# -- Event hook --------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def on_profile_switch(self) -> Callable[[Profile], None] | None:
|
|
132
|
+
return self._on_switch
|
|
133
|
+
|
|
134
|
+
@on_profile_switch.setter
|
|
135
|
+
def on_profile_switch(self, cb: Callable[[Profile], None] | None) -> None:
|
|
136
|
+
self._on_switch = cb
|
|
137
|
+
|
|
138
|
+
# -- CRUD --------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def create_profile(
|
|
141
|
+
self,
|
|
142
|
+
name: str,
|
|
143
|
+
description: str = "",
|
|
144
|
+
personality: str = "",
|
|
145
|
+
mode: Mode = Mode.A,
|
|
146
|
+
config: dict[str, Any] | None = None,
|
|
147
|
+
) -> Profile:
|
|
148
|
+
"""Create a new profile. Raises ValueError on duplicate or bad name."""
|
|
149
|
+
_validate_name(name)
|
|
150
|
+
if name in self._profiles:
|
|
151
|
+
raise ValueError(f"Profile '{name}' already exists.")
|
|
152
|
+
profile = Profile(
|
|
153
|
+
profile_id=_pid(), name=name, description=description,
|
|
154
|
+
personality=personality, mode=mode,
|
|
155
|
+
created_at=_now(), config=config or {},
|
|
156
|
+
)
|
|
157
|
+
self._profiles[name] = profile
|
|
158
|
+
self._save()
|
|
159
|
+
logger.info("Created profile '%s' (id=%s, mode=%s)", name, profile.profile_id, mode.value)
|
|
160
|
+
return profile
|
|
161
|
+
|
|
162
|
+
def get_profile(self, name: str) -> Profile | None:
|
|
163
|
+
"""Return profile by name, or None if not found."""
|
|
164
|
+
return self._profiles.get(name)
|
|
165
|
+
|
|
166
|
+
def get_active_profile(self) -> Profile:
|
|
167
|
+
"""Return the currently active profile."""
|
|
168
|
+
return self._profiles.get(self._active_name) or self._profiles[_RESERVED]
|
|
169
|
+
|
|
170
|
+
def switch_profile(self, name: str) -> Profile:
|
|
171
|
+
"""Instant switch — config-only, zero data movement."""
|
|
172
|
+
if name not in self._profiles:
|
|
173
|
+
raise KeyError(f"Profile '{name}' does not exist.")
|
|
174
|
+
self._active_name = name
|
|
175
|
+
updated = _evolve(self._profiles[name], last_used=_now())
|
|
176
|
+
self._profiles[name] = updated
|
|
177
|
+
self._save()
|
|
178
|
+
logger.info("Switched to profile '%s'", name)
|
|
179
|
+
if self._on_switch is not None:
|
|
180
|
+
self._on_switch(updated)
|
|
181
|
+
return updated
|
|
182
|
+
|
|
183
|
+
def list_profiles(self) -> list[Profile]:
|
|
184
|
+
"""All profiles sorted by name (default first)."""
|
|
185
|
+
profs = list(self._profiles.values())
|
|
186
|
+
profs.sort(key=lambda p: (p.name != _RESERVED, p.name))
|
|
187
|
+
return profs
|
|
188
|
+
|
|
189
|
+
def delete_profile(self, name: str) -> None:
|
|
190
|
+
"""Delete a profile. Cannot delete 'default'. Data stays in DB."""
|
|
191
|
+
if name == _RESERVED:
|
|
192
|
+
raise ValueError("Cannot delete the default profile.")
|
|
193
|
+
if name not in self._profiles:
|
|
194
|
+
raise KeyError(f"Profile '{name}' does not exist.")
|
|
195
|
+
del self._profiles[name]
|
|
196
|
+
if self._active_name == name:
|
|
197
|
+
self._active_name = _RESERVED
|
|
198
|
+
logger.info("Active profile deleted — fell back to 'default'.")
|
|
199
|
+
self._save()
|
|
200
|
+
logger.info("Deleted profile '%s'. Associated data remains in DB.", name)
|
|
201
|
+
|
|
202
|
+
def rename_profile(self, old_name: str, new_name: str) -> None:
|
|
203
|
+
"""Rename a profile. Cannot rename 'default'."""
|
|
204
|
+
if old_name == _RESERVED:
|
|
205
|
+
raise ValueError("Cannot rename the default profile.")
|
|
206
|
+
_validate_name(new_name)
|
|
207
|
+
if old_name not in self._profiles:
|
|
208
|
+
raise KeyError(f"Profile '{old_name}' does not exist.")
|
|
209
|
+
if new_name in self._profiles:
|
|
210
|
+
raise ValueError(f"Profile '{new_name}' already exists.")
|
|
211
|
+
old = self._profiles.pop(old_name)
|
|
212
|
+
self._profiles[new_name] = _evolve(old, name=new_name)
|
|
213
|
+
if self._active_name == old_name:
|
|
214
|
+
self._active_name = new_name
|
|
215
|
+
self._save()
|
|
216
|
+
logger.info("Renamed profile '%s' -> '%s'", old_name, new_name)
|
|
217
|
+
|
|
218
|
+
def update_profile(self, name: str, **kwargs: Any) -> Profile:
|
|
219
|
+
"""Update mutable fields: description, personality, mode, config."""
|
|
220
|
+
if name not in self._profiles:
|
|
221
|
+
raise KeyError(f"Profile '{name}' does not exist.")
|
|
222
|
+
allowed = {"description", "personality", "mode", "config"}
|
|
223
|
+
overrides = {k: v for k, v in kwargs.items() if k in allowed}
|
|
224
|
+
updated = _evolve(self._profiles[name], **overrides)
|
|
225
|
+
self._profiles[name] = updated
|
|
226
|
+
self._save()
|
|
227
|
+
logger.info("Updated profile '%s'", name)
|
|
228
|
+
return updated
|
|
229
|
+
|
|
230
|
+
def export_profile(self, name: str) -> dict[str, Any]:
|
|
231
|
+
"""Export profile metadata as a plain dict (for backup / migration)."""
|
|
232
|
+
if name not in self._profiles:
|
|
233
|
+
raise KeyError(f"Profile '{name}' does not exist.")
|
|
234
|
+
return asdict(self._profiles[name])
|