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,99 @@
|
|
|
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
|
+
"""Rules engine for configurable auto-capture and auto-recall behavior."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
DEFAULT_RULES = {
|
|
17
|
+
"auto_recall": {
|
|
18
|
+
"enabled": True,
|
|
19
|
+
"on_session_start": True,
|
|
20
|
+
"on_every_prompt": False,
|
|
21
|
+
"max_memories_injected": 10,
|
|
22
|
+
"relevance_threshold": 0.3,
|
|
23
|
+
},
|
|
24
|
+
"auto_capture": {
|
|
25
|
+
"enabled": True,
|
|
26
|
+
"capture_decisions": True,
|
|
27
|
+
"capture_bugs": True,
|
|
28
|
+
"capture_preferences": True,
|
|
29
|
+
"capture_session_summary": True,
|
|
30
|
+
"min_confidence": 0.5,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RulesEngine:
|
|
36
|
+
"""Manage configurable rules for auto-capture and auto-recall."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: dict | None = None, config_path: Path | None = None):
|
|
39
|
+
if config:
|
|
40
|
+
self._rules = {**DEFAULT_RULES, **config}
|
|
41
|
+
elif config_path and config_path.exists():
|
|
42
|
+
data = json.loads(config_path.read_text())
|
|
43
|
+
self._rules = {**DEFAULT_RULES, **data.get("rules", {})}
|
|
44
|
+
else:
|
|
45
|
+
self._rules = dict(DEFAULT_RULES)
|
|
46
|
+
|
|
47
|
+
def should_capture(self, category: str, confidence: float) -> bool:
|
|
48
|
+
"""Check if a category should be captured based on rules."""
|
|
49
|
+
capture_rules = self._rules.get("auto_capture", {})
|
|
50
|
+
if not capture_rules.get("enabled", True):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
category_key = f"capture_{category}s" if not category.endswith("s") else f"capture_{category}"
|
|
54
|
+
if not capture_rules.get(category_key, True):
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
min_conf = capture_rules.get("min_confidence", 0.5)
|
|
58
|
+
return confidence >= min_conf
|
|
59
|
+
|
|
60
|
+
def should_recall(self, trigger: str) -> bool:
|
|
61
|
+
"""Check if auto-recall should fire for a trigger."""
|
|
62
|
+
recall_rules = self._rules.get("auto_recall", {})
|
|
63
|
+
if not recall_rules.get("enabled", True):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
if trigger == "session_start":
|
|
67
|
+
return recall_rules.get("on_session_start", True)
|
|
68
|
+
if trigger == "every_prompt":
|
|
69
|
+
return recall_rules.get("on_every_prompt", False)
|
|
70
|
+
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def get_recall_config(self) -> dict:
|
|
74
|
+
"""Get auto-recall configuration."""
|
|
75
|
+
return dict(self._rules.get("auto_recall", DEFAULT_RULES["auto_recall"]))
|
|
76
|
+
|
|
77
|
+
def get_capture_config(self) -> dict:
|
|
78
|
+
"""Get auto-capture configuration."""
|
|
79
|
+
return dict(self._rules.get("auto_capture", DEFAULT_RULES["auto_capture"]))
|
|
80
|
+
|
|
81
|
+
def update_rule(self, section: str, key: str, value: Any) -> None:
|
|
82
|
+
"""Update a specific rule."""
|
|
83
|
+
if section not in self._rules:
|
|
84
|
+
self._rules[section] = {}
|
|
85
|
+
self._rules[section][key] = value
|
|
86
|
+
|
|
87
|
+
def save(self, config_path: Path) -> None:
|
|
88
|
+
"""Save rules to config file."""
|
|
89
|
+
if config_path.exists():
|
|
90
|
+
data = json.loads(config_path.read_text())
|
|
91
|
+
else:
|
|
92
|
+
data = {}
|
|
93
|
+
data["rules"] = self._rules
|
|
94
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
config_path.write_text(json.dumps(data, indent=2))
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> dict:
|
|
98
|
+
"""Export rules as dict."""
|
|
99
|
+
return dict(self._rules)
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
"""Opt-in API-key authentication middleware.
|
|
5
|
+
|
|
6
|
+
When ``~/.superlocalmemory/api_key`` exists, write endpoints require the
|
|
7
|
+
``X-SLM-API-Key`` header. Read endpoints remain open for backward
|
|
8
|
+
compatibility. If the key file is absent auth is completely disabled --
|
|
9
|
+
all requests pass.
|
|
10
|
+
|
|
11
|
+
V3 change: base directory moved from ``~/.claude-memory/`` to
|
|
12
|
+
``~/.superlocalmemory/``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("superlocalmemory.auth")
|
|
21
|
+
|
|
22
|
+
# V3 base directory
|
|
23
|
+
MEMORY_DIR = Path.home() / ".superlocalmemory"
|
|
24
|
+
API_KEY_FILE = MEMORY_DIR / "api_key"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_api_key_hash(key_file: Optional[Path] = None) -> Optional[str]:
|
|
28
|
+
"""Load and hash the API key from disk.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
key_file: Override path (useful for testing).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
SHA-256 hex digest of the stored key, or ``None`` when auth is
|
|
35
|
+
not configured.
|
|
36
|
+
"""
|
|
37
|
+
path = key_file or API_KEY_FILE
|
|
38
|
+
if not path.exists():
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
key = path.read_text().strip()
|
|
42
|
+
if not key:
|
|
43
|
+
return None
|
|
44
|
+
return hashlib.sha256(key.encode()).hexdigest()
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
logger.warning("Failed to load API key: %s", exc)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_api_key(
|
|
51
|
+
request_headers: dict,
|
|
52
|
+
is_write: bool = False,
|
|
53
|
+
key_file: Optional[Path] = None,
|
|
54
|
+
) -> bool:
|
|
55
|
+
"""Authorize a request against the stored API key.
|
|
56
|
+
|
|
57
|
+
Returns ``True`` when:
|
|
58
|
+
* No key file exists (auth not configured -- backward compatible).
|
|
59
|
+
* The request is a read operation (reads always allowed).
|
|
60
|
+
* The ``X-SLM-API-Key`` header matches the stored key.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
request_headers: Mapping of HTTP header names to values.
|
|
64
|
+
is_write: ``True`` for mutating operations that require auth.
|
|
65
|
+
key_file: Override key-file path (testing).
|
|
66
|
+
"""
|
|
67
|
+
key_hash = _load_api_key_hash(key_file)
|
|
68
|
+
|
|
69
|
+
# No key file = auth disabled
|
|
70
|
+
if key_hash is None:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
# Reads are always permitted
|
|
74
|
+
if not is_write:
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
# Writes require a matching key
|
|
78
|
+
provided = request_headers.get("x-slm-api-key", "")
|
|
79
|
+
if not provided:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
return hashlib.sha256(provided.encode()).hexdigest() == key_hash
|
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
"""Automated backup manager for SuperLocalMemory V3.
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
* Configurable interval (daily / weekly)
|
|
8
|
+
* Timestamped SQLite-safe backups via the ``sqlite3.backup()`` API
|
|
9
|
+
* Retention policy (keeps last *N* backups)
|
|
10
|
+
* Restore with automatic pre-restore safety snapshot
|
|
11
|
+
|
|
12
|
+
V3 change: base directory is ``~/.superlocalmemory/`` (was ``~/.claude-memory/``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import sqlite3
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("superlocalmemory.backup")
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# V3 paths
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
MEMORY_DIR = Path.home() / ".superlocalmemory"
|
|
28
|
+
DB_PATH = MEMORY_DIR / "memory.db"
|
|
29
|
+
BACKUP_DIR = MEMORY_DIR / "backups"
|
|
30
|
+
CONFIG_FILE = MEMORY_DIR / "backup_config.json"
|
|
31
|
+
|
|
32
|
+
# Defaults
|
|
33
|
+
DEFAULT_INTERVAL_HOURS = 168 # 7 days
|
|
34
|
+
DEFAULT_MAX_BACKUPS = 10
|
|
35
|
+
MIN_INTERVAL_HOURS = 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BackupManager:
|
|
39
|
+
"""Automated backup manager for SuperLocalMemory V3.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
db_path: Path to the primary database file.
|
|
43
|
+
backup_dir: Directory where backup files are stored.
|
|
44
|
+
base_dir: Base SLM directory (used for config file + learning DB).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
db_path: Optional[Path] = None,
|
|
50
|
+
backup_dir: Optional[Path] = None,
|
|
51
|
+
base_dir: Optional[Path] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self.base_dir = base_dir or MEMORY_DIR
|
|
54
|
+
self.db_path = db_path or (self.base_dir / "memory.db")
|
|
55
|
+
self.backup_dir = backup_dir or (self.base_dir / "backups")
|
|
56
|
+
self._config_file = self.base_dir / "backup_config.json"
|
|
57
|
+
self.config = self._load_config()
|
|
58
|
+
self._ensure_backup_dir()
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
# Config management
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def _ensure_backup_dir(self) -> None:
|
|
65
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
def _load_config(self) -> Dict:
|
|
68
|
+
if self._config_file.exists():
|
|
69
|
+
try:
|
|
70
|
+
raw = json.loads(self._config_file.read_text())
|
|
71
|
+
defaults = self._default_config()
|
|
72
|
+
for k in defaults:
|
|
73
|
+
raw.setdefault(k, defaults[k])
|
|
74
|
+
return raw
|
|
75
|
+
except (json.JSONDecodeError, IOError):
|
|
76
|
+
pass
|
|
77
|
+
return self._default_config()
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _default_config() -> Dict:
|
|
81
|
+
return {
|
|
82
|
+
"enabled": True,
|
|
83
|
+
"interval_hours": DEFAULT_INTERVAL_HOURS,
|
|
84
|
+
"max_backups": DEFAULT_MAX_BACKUPS,
|
|
85
|
+
"last_backup": None,
|
|
86
|
+
"last_backup_file": None,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def _save_config(self) -> None:
|
|
90
|
+
try:
|
|
91
|
+
self._config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
self._config_file.write_text(json.dumps(self.config, indent=2))
|
|
93
|
+
except IOError as exc:
|
|
94
|
+
logger.error("Failed to save backup config: %s", exc)
|
|
95
|
+
|
|
96
|
+
def configure(
|
|
97
|
+
self,
|
|
98
|
+
interval_hours: Optional[int] = None,
|
|
99
|
+
max_backups: Optional[int] = None,
|
|
100
|
+
enabled: Optional[bool] = None,
|
|
101
|
+
) -> Dict:
|
|
102
|
+
"""Update backup configuration and return current status."""
|
|
103
|
+
if interval_hours is not None:
|
|
104
|
+
self.config["interval_hours"] = max(MIN_INTERVAL_HOURS, interval_hours)
|
|
105
|
+
if max_backups is not None:
|
|
106
|
+
self.config["max_backups"] = max(1, max_backups)
|
|
107
|
+
if enabled is not None:
|
|
108
|
+
self.config["enabled"] = enabled
|
|
109
|
+
self._save_config()
|
|
110
|
+
return self.get_status()
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Scheduling helpers
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def is_backup_due(self) -> bool:
|
|
117
|
+
"""Return ``True`` when a backup should be taken."""
|
|
118
|
+
if not self.config.get("enabled", True):
|
|
119
|
+
return False
|
|
120
|
+
last = self.config.get("last_backup")
|
|
121
|
+
if not last:
|
|
122
|
+
return True
|
|
123
|
+
try:
|
|
124
|
+
last_dt = datetime.fromisoformat(last)
|
|
125
|
+
interval = timedelta(hours=self.config.get("interval_hours", DEFAULT_INTERVAL_HOURS))
|
|
126
|
+
return datetime.now() >= last_dt + interval
|
|
127
|
+
except (ValueError, TypeError):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def check_and_backup(self) -> Optional[str]:
|
|
131
|
+
"""Create a backup only when one is due. Returns filename or ``None``."""
|
|
132
|
+
if not self.is_backup_due():
|
|
133
|
+
return None
|
|
134
|
+
return self.create_backup()
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
# Core backup / restore
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def create_backup(self, label: Optional[str] = None) -> str:
|
|
141
|
+
"""Create a timestamped backup via the SQLite online-backup API.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Backup filename on success, empty string on failure.
|
|
145
|
+
"""
|
|
146
|
+
if not self.db_path.exists():
|
|
147
|
+
logger.warning("No database to backup at %s", self.db_path)
|
|
148
|
+
return ""
|
|
149
|
+
|
|
150
|
+
self._ensure_backup_dir()
|
|
151
|
+
|
|
152
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
153
|
+
suffix = f"-{label}" if label else ""
|
|
154
|
+
backup_name = f"memory-{timestamp}{suffix}.db"
|
|
155
|
+
backup_path = self.backup_dir / backup_name
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
source = sqlite3.connect(str(self.db_path))
|
|
159
|
+
dest = sqlite3.connect(str(backup_path))
|
|
160
|
+
try:
|
|
161
|
+
source.backup(dest)
|
|
162
|
+
finally:
|
|
163
|
+
dest.close()
|
|
164
|
+
source.close()
|
|
165
|
+
|
|
166
|
+
size_mb = backup_path.stat().st_size / (1024 * 1024)
|
|
167
|
+
self.config["last_backup"] = datetime.now().isoformat()
|
|
168
|
+
self.config["last_backup_file"] = backup_name
|
|
169
|
+
self._save_config()
|
|
170
|
+
logger.info("Backup created: %s (%.1f MB)", backup_name, size_mb)
|
|
171
|
+
|
|
172
|
+
# Also backup learning.db if present
|
|
173
|
+
self._backup_learning_db(timestamp, suffix)
|
|
174
|
+
|
|
175
|
+
self._enforce_retention()
|
|
176
|
+
return backup_name
|
|
177
|
+
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
logger.error("Backup failed: %s", exc)
|
|
180
|
+
if backup_path.exists():
|
|
181
|
+
backup_path.unlink()
|
|
182
|
+
return ""
|
|
183
|
+
|
|
184
|
+
def _backup_learning_db(self, timestamp: str, suffix: str) -> None:
|
|
185
|
+
"""Best-effort backup of ``learning.db`` alongside the main DB."""
|
|
186
|
+
learning_db = self.db_path.parent / "learning.db"
|
|
187
|
+
if not learning_db.exists():
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
name = f"learning-{timestamp}{suffix}.db"
|
|
191
|
+
path = self.backup_dir / name
|
|
192
|
+
src = sqlite3.connect(str(learning_db))
|
|
193
|
+
dst = sqlite3.connect(str(path))
|
|
194
|
+
try:
|
|
195
|
+
src.backup(dst)
|
|
196
|
+
finally:
|
|
197
|
+
dst.close()
|
|
198
|
+
src.close()
|
|
199
|
+
logger.info("Learning backup: %s (%.1f MB)", name, path.stat().st_size / (1024 * 1024))
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
logger.warning("Learning DB backup failed (non-critical): %s", exc)
|
|
202
|
+
|
|
203
|
+
def _enforce_retention(self) -> None:
|
|
204
|
+
"""Remove old backups exceeding the configured max."""
|
|
205
|
+
max_backups = self.config.get("max_backups", DEFAULT_MAX_BACKUPS)
|
|
206
|
+
for pattern in ("memory-*.db", "learning-*.db"):
|
|
207
|
+
backups = sorted(
|
|
208
|
+
self.backup_dir.glob(pattern),
|
|
209
|
+
key=lambda f: f.stat().st_mtime,
|
|
210
|
+
)
|
|
211
|
+
while len(backups) > max_backups:
|
|
212
|
+
oldest = backups.pop(0)
|
|
213
|
+
try:
|
|
214
|
+
oldest.unlink()
|
|
215
|
+
logger.info("Removed old backup: %s", oldest.name)
|
|
216
|
+
except OSError as exc:
|
|
217
|
+
logger.error("Failed to remove backup %s: %s", oldest.name, exc)
|
|
218
|
+
|
|
219
|
+
def restore_backup(self, filename: str) -> bool:
|
|
220
|
+
"""Restore the database from *filename*.
|
|
221
|
+
|
|
222
|
+
A safety snapshot of the current state is taken first.
|
|
223
|
+
"""
|
|
224
|
+
backup_path = self.backup_dir / filename
|
|
225
|
+
if not backup_path.exists():
|
|
226
|
+
logger.error("Backup not found: %s", filename)
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
self.create_backup(label="pre-restore")
|
|
231
|
+
|
|
232
|
+
target = (
|
|
233
|
+
self.db_path.parent / "learning.db"
|
|
234
|
+
if filename.startswith("learning-")
|
|
235
|
+
else self.db_path
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
src = sqlite3.connect(str(backup_path))
|
|
239
|
+
dst = sqlite3.connect(str(target))
|
|
240
|
+
try:
|
|
241
|
+
src.backup(dst)
|
|
242
|
+
finally:
|
|
243
|
+
dst.close()
|
|
244
|
+
src.close()
|
|
245
|
+
|
|
246
|
+
logger.info("Restored: %s -> %s", filename, target.name)
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
except Exception as exc:
|
|
250
|
+
logger.error("Restore failed: %s", exc)
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
# Listing / status
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def list_backups(self) -> List[Dict]:
|
|
258
|
+
"""Return metadata for all available backups (newest first)."""
|
|
259
|
+
if not self.backup_dir.exists():
|
|
260
|
+
return []
|
|
261
|
+
|
|
262
|
+
result: List[Dict] = []
|
|
263
|
+
for pattern in ("memory-*.db", "learning-*.db"):
|
|
264
|
+
for f in sorted(self.backup_dir.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True):
|
|
265
|
+
st = f.stat()
|
|
266
|
+
db_type = "learning" if f.name.startswith("learning-") else "memory"
|
|
267
|
+
result.append({
|
|
268
|
+
"filename": f.name,
|
|
269
|
+
"path": str(f),
|
|
270
|
+
"size_mb": round(st.st_size / (1024 * 1024), 2),
|
|
271
|
+
"created": datetime.fromtimestamp(st.st_mtime).isoformat(),
|
|
272
|
+
"age_hours": round(
|
|
273
|
+
(datetime.now() - datetime.fromtimestamp(st.st_mtime)).total_seconds() / 3600, 1
|
|
274
|
+
),
|
|
275
|
+
"type": db_type,
|
|
276
|
+
})
|
|
277
|
+
result.sort(key=lambda b: b["created"], reverse=True)
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
def get_status(self) -> Dict:
|
|
281
|
+
"""Return a status summary of the backup system."""
|
|
282
|
+
backups = self.list_backups()
|
|
283
|
+
next_backup = None
|
|
284
|
+
|
|
285
|
+
if self.config.get("enabled") and self.config.get("last_backup"):
|
|
286
|
+
try:
|
|
287
|
+
last_dt = datetime.fromisoformat(self.config["last_backup"])
|
|
288
|
+
interval = timedelta(hours=self.config.get("interval_hours", DEFAULT_INTERVAL_HOURS))
|
|
289
|
+
nxt = last_dt + interval
|
|
290
|
+
next_backup = nxt.isoformat() if nxt > datetime.now() else "overdue"
|
|
291
|
+
except (ValueError, TypeError):
|
|
292
|
+
next_backup = "unknown"
|
|
293
|
+
|
|
294
|
+
hours = self.config.get("interval_hours", DEFAULT_INTERVAL_HOURS)
|
|
295
|
+
if hours >= 168:
|
|
296
|
+
display = f"{hours // 168} week(s)"
|
|
297
|
+
elif hours >= 24:
|
|
298
|
+
display = f"{hours // 24} day(s)"
|
|
299
|
+
else:
|
|
300
|
+
display = f"{hours} hour(s)"
|
|
301
|
+
|
|
302
|
+
mem_bk = [b for b in backups if b.get("type") == "memory"]
|
|
303
|
+
learn_bk = [b for b in backups if b.get("type") == "learning"]
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"enabled": self.config.get("enabled", True),
|
|
307
|
+
"interval_hours": hours,
|
|
308
|
+
"interval_display": display,
|
|
309
|
+
"max_backups": self.config.get("max_backups", DEFAULT_MAX_BACKUPS),
|
|
310
|
+
"last_backup": self.config.get("last_backup"),
|
|
311
|
+
"last_backup_file": self.config.get("last_backup_file"),
|
|
312
|
+
"next_backup": next_backup,
|
|
313
|
+
"backup_count": len(mem_bk),
|
|
314
|
+
"learning_backup_count": len(learn_bk),
|
|
315
|
+
"total_size_mb": round(sum(b["size_mb"] for b in backups), 2),
|
|
316
|
+
"backups": backups,
|
|
317
|
+
}
|