superlocalmemory 2.8.6 → 3.0.1
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 +62 -48
- 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
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
-
"""
|
|
5
|
-
ProjectContextManager — Layer 2: Multi-signal project detection.
|
|
6
|
-
|
|
7
|
-
Detects the current active project using 4 weighted signals, not just
|
|
8
|
-
the explicit project_name tag. This improves recall by boosting memories
|
|
9
|
-
from the currently active project context.
|
|
10
|
-
|
|
11
|
-
Signal architecture:
|
|
12
|
-
1. project_tag (weight 3) — Explicit project_name field in memories
|
|
13
|
-
2. project_path (weight 2) — File path analysis (extract project dir)
|
|
14
|
-
3. active_profile (weight 1) — Profile name (weak signal)
|
|
15
|
-
4. content_cluster (weight 1) — Cluster co-occurrence in recent memories
|
|
16
|
-
|
|
17
|
-
Winner-take-all with 40% threshold: the candidate project must accumulate
|
|
18
|
-
more than 40% of the total weighted signal to be declared the current
|
|
19
|
-
project. If no candidate clears the threshold, returns None (ambiguous).
|
|
20
|
-
|
|
21
|
-
Design principles:
|
|
22
|
-
- Reads memory.db in READ-ONLY mode (never writes to memory.db)
|
|
23
|
-
- Handles missing columns gracefully (older DBs lack project_name)
|
|
24
|
-
- Thread-safe: each method opens/closes its own connection
|
|
25
|
-
- Zero external dependencies (pure stdlib)
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
import json
|
|
29
|
-
import logging
|
|
30
|
-
import sqlite3
|
|
31
|
-
import threading
|
|
32
|
-
from collections import Counter
|
|
33
|
-
from pathlib import Path
|
|
34
|
-
from typing import Optional, List, Dict, Any
|
|
35
|
-
|
|
36
|
-
logger = logging.getLogger("superlocalmemory.learning.project_context")
|
|
37
|
-
|
|
38
|
-
MEMORY_DIR = Path.home() / ".claude-memory"
|
|
39
|
-
MEMORY_DB_PATH = MEMORY_DIR / "memory.db"
|
|
40
|
-
PROFILES_JSON = MEMORY_DIR / "profiles.json"
|
|
41
|
-
|
|
42
|
-
# Directories commonly found as parents of project roots.
|
|
43
|
-
# Used by _extract_project_from_path to identify where the
|
|
44
|
-
# project directory begins in a file path.
|
|
45
|
-
_PROJECT_PARENT_DIRS = frozenset({
|
|
46
|
-
"projects", "repos", "repositories", "workspace", "workspaces",
|
|
47
|
-
"code", "development", "github", "gitlab",
|
|
48
|
-
"bitbucket", "Documents", "sites", "apps", "services",
|
|
49
|
-
"AGENTIC_Official", # Varun's workspace convention
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
# Directories that are NOT project names (too generic / too deep).
|
|
53
|
-
_SKIP_DIRS = frozenset({
|
|
54
|
-
"src", "lib", "bin", "node_modules", "venv", ".venv", "env",
|
|
55
|
-
".git", "__pycache__", "dist", "build", "target", "out",
|
|
56
|
-
".cache", ".config", "tmp", "temp", "logs", "test", "tests",
|
|
57
|
-
"vendor", "packages", "deps",
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class ProjectContextManager:
|
|
62
|
-
"""
|
|
63
|
-
Detects the currently active project using multi-signal analysis.
|
|
64
|
-
|
|
65
|
-
Usage:
|
|
66
|
-
pcm = ProjectContextManager()
|
|
67
|
-
project = pcm.detect_current_project()
|
|
68
|
-
if project:
|
|
69
|
-
boost = pcm.get_project_boost(memory, project)
|
|
70
|
-
|
|
71
|
-
Thread-safe: safe to call from multiple agents / MCP handlers.
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
SIGNAL_WEIGHTS: Dict[str, int] = {
|
|
75
|
-
'project_tag': 3, # Explicit project_name field
|
|
76
|
-
'project_path': 2, # File path analysis
|
|
77
|
-
'active_profile': 1, # Profile name (weak signal)
|
|
78
|
-
'content_cluster': 1, # Cluster co-occurrence
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
def __init__(self, memory_db_path: Optional[Path] = None):
|
|
82
|
-
"""
|
|
83
|
-
Initialize ProjectContextManager.
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
memory_db_path: Path to memory.db. Defaults to
|
|
87
|
-
~/.claude-memory/memory.db. Opened read-only.
|
|
88
|
-
"""
|
|
89
|
-
self._memory_db_path = Path(memory_db_path) if memory_db_path else MEMORY_DB_PATH
|
|
90
|
-
self._lock = threading.Lock()
|
|
91
|
-
# Cache available columns to avoid repeated PRAGMA calls
|
|
92
|
-
self._available_columns: Optional[set] = None
|
|
93
|
-
logger.info(
|
|
94
|
-
"ProjectContextManager initialized: db=%s",
|
|
95
|
-
self._memory_db_path,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
# ------------------------------------------------------------------
|
|
99
|
-
# Public API
|
|
100
|
-
# ------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
def detect_current_project(
|
|
103
|
-
self,
|
|
104
|
-
recent_memories: Optional[List[Dict[str, Any]]] = None,
|
|
105
|
-
) -> Optional[str]:
|
|
106
|
-
"""
|
|
107
|
-
Detect the currently active project from recent memory activity.
|
|
108
|
-
|
|
109
|
-
Applies 4 weighted signals. The winner must accumulate >40% of the
|
|
110
|
-
total weighted signal to be declared current. Returns None when
|
|
111
|
-
ambiguous or insufficient data.
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
recent_memories: Pre-fetched list of memory dicts.
|
|
115
|
-
If None, the last 20 memories are fetched from memory.db.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
Project name string or None if undetermined.
|
|
119
|
-
"""
|
|
120
|
-
if recent_memories is None:
|
|
121
|
-
recent_memories = self._get_recent_memories(limit=20)
|
|
122
|
-
|
|
123
|
-
if not recent_memories:
|
|
124
|
-
logger.debug("No recent memories — cannot detect project")
|
|
125
|
-
return None
|
|
126
|
-
|
|
127
|
-
# Accumulate weighted votes per candidate project
|
|
128
|
-
votes: Counter = Counter()
|
|
129
|
-
|
|
130
|
-
# --- Signal 1: project_tag (weight 3) ---
|
|
131
|
-
for mem in recent_memories:
|
|
132
|
-
pname = self._safe_get(mem, 'project_name')
|
|
133
|
-
if pname:
|
|
134
|
-
votes[pname] += self.SIGNAL_WEIGHTS['project_tag']
|
|
135
|
-
|
|
136
|
-
# --- Signal 2: project_path (weight 2) ---
|
|
137
|
-
for mem in recent_memories:
|
|
138
|
-
ppath = self._safe_get(mem, 'project_path')
|
|
139
|
-
if ppath:
|
|
140
|
-
extracted = self._extract_project_from_path(ppath)
|
|
141
|
-
if extracted:
|
|
142
|
-
votes[extracted] += self.SIGNAL_WEIGHTS['project_path']
|
|
143
|
-
|
|
144
|
-
# --- Signal 3: active_profile (weight 1) ---
|
|
145
|
-
# Profile is a weak signal: only contributes if it matches a
|
|
146
|
-
# project name that already has some votes.
|
|
147
|
-
active_profile = self._get_active_profile()
|
|
148
|
-
if active_profile and active_profile != 'default':
|
|
149
|
-
# If profile name coincides with an existing candidate, boost it.
|
|
150
|
-
# If not, add it as a weak standalone candidate.
|
|
151
|
-
votes[active_profile] += self.SIGNAL_WEIGHTS['active_profile']
|
|
152
|
-
|
|
153
|
-
# --- Signal 4: content_cluster (weight 1) ---
|
|
154
|
-
cluster_project = self._match_content_to_clusters(recent_memories)
|
|
155
|
-
if cluster_project:
|
|
156
|
-
votes[cluster_project] += self.SIGNAL_WEIGHTS['content_cluster']
|
|
157
|
-
|
|
158
|
-
if not votes:
|
|
159
|
-
logger.debug("No project signals detected in recent memories")
|
|
160
|
-
return None
|
|
161
|
-
|
|
162
|
-
# Winner-take-all with 40% threshold
|
|
163
|
-
total_weight = sum(votes.values())
|
|
164
|
-
winner, winner_weight = votes.most_common(1)[0]
|
|
165
|
-
winner_ratio = winner_weight / total_weight if total_weight > 0 else 0.0
|
|
166
|
-
|
|
167
|
-
if winner_ratio > 0.4:
|
|
168
|
-
logger.debug(
|
|
169
|
-
"Project detected: '%s' (%.0f%% of signal, %d total weight)",
|
|
170
|
-
winner, winner_ratio * 100, total_weight,
|
|
171
|
-
)
|
|
172
|
-
return winner
|
|
173
|
-
|
|
174
|
-
logger.debug(
|
|
175
|
-
"No clear project winner: top='%s' at %.0f%% (threshold 40%%)",
|
|
176
|
-
winner, winner_ratio * 100,
|
|
177
|
-
)
|
|
178
|
-
return None
|
|
179
|
-
|
|
180
|
-
def get_project_boost(
|
|
181
|
-
self,
|
|
182
|
-
memory: Dict[str, Any],
|
|
183
|
-
current_project: Optional[str] = None,
|
|
184
|
-
) -> float:
|
|
185
|
-
"""
|
|
186
|
-
Return a boost factor for ranking based on project match.
|
|
187
|
-
|
|
188
|
-
Args:
|
|
189
|
-
memory: A memory dict with at least 'project_name' or
|
|
190
|
-
'project_path' fields.
|
|
191
|
-
current_project: The detected current project (from
|
|
192
|
-
detect_current_project). If None, returns neutral.
|
|
193
|
-
|
|
194
|
-
Returns:
|
|
195
|
-
1.0 — memory matches current project (boost)
|
|
196
|
-
0.6 — project unknown or memory has no project info (neutral)
|
|
197
|
-
0.3 — memory belongs to a different project (penalty)
|
|
198
|
-
"""
|
|
199
|
-
if current_project is None:
|
|
200
|
-
return 0.6 # Unknown project context — neutral
|
|
201
|
-
|
|
202
|
-
# Check explicit project_name
|
|
203
|
-
mem_project = self._safe_get(memory, 'project_name')
|
|
204
|
-
if mem_project:
|
|
205
|
-
if mem_project.lower() == current_project.lower():
|
|
206
|
-
return 1.0
|
|
207
|
-
return 0.3 # Definite mismatch
|
|
208
|
-
|
|
209
|
-
# Check project_path
|
|
210
|
-
mem_path = self._safe_get(memory, 'project_path')
|
|
211
|
-
if mem_path:
|
|
212
|
-
extracted = self._extract_project_from_path(mem_path)
|
|
213
|
-
if extracted:
|
|
214
|
-
if extracted.lower() == current_project.lower():
|
|
215
|
-
return 1.0
|
|
216
|
-
return 0.3 # Definite mismatch
|
|
217
|
-
|
|
218
|
-
# Memory has no project info — neutral
|
|
219
|
-
return 0.6
|
|
220
|
-
|
|
221
|
-
# ------------------------------------------------------------------
|
|
222
|
-
# Data fetching (memory.db — read-only)
|
|
223
|
-
# ------------------------------------------------------------------
|
|
224
|
-
|
|
225
|
-
def _get_recent_memories(self, limit: int = 20) -> List[Dict[str, Any]]:
|
|
226
|
-
"""
|
|
227
|
-
Fetch the most recent memories from memory.db.
|
|
228
|
-
|
|
229
|
-
Returns a list of dicts with available columns. Handles missing
|
|
230
|
-
columns gracefully (older databases may lack project_name, etc.).
|
|
231
|
-
"""
|
|
232
|
-
if not self._memory_db_path.exists():
|
|
233
|
-
logger.debug("memory.db not found at %s", self._memory_db_path)
|
|
234
|
-
return []
|
|
235
|
-
|
|
236
|
-
available = self._get_available_columns()
|
|
237
|
-
|
|
238
|
-
# Build SELECT with only available columns
|
|
239
|
-
desired_cols = [
|
|
240
|
-
'id', 'project_name', 'project_path', 'profile',
|
|
241
|
-
'content', 'cluster_id', 'created_at',
|
|
242
|
-
]
|
|
243
|
-
select_cols = [c for c in desired_cols if c in available]
|
|
244
|
-
|
|
245
|
-
if not select_cols:
|
|
246
|
-
logger.warning("memories table has none of the expected columns")
|
|
247
|
-
return []
|
|
248
|
-
|
|
249
|
-
# Always need at least 'id' — if missing, bail
|
|
250
|
-
if 'id' not in available:
|
|
251
|
-
return []
|
|
252
|
-
|
|
253
|
-
col_list = ", ".join(select_cols)
|
|
254
|
-
|
|
255
|
-
# Build ORDER BY using best available timestamp
|
|
256
|
-
order_col = 'created_at' if 'created_at' in available else 'id'
|
|
257
|
-
|
|
258
|
-
try:
|
|
259
|
-
conn = self._open_memory_db()
|
|
260
|
-
try:
|
|
261
|
-
cursor = conn.cursor()
|
|
262
|
-
cursor.execute(
|
|
263
|
-
f"SELECT {col_list} FROM memories "
|
|
264
|
-
f"ORDER BY {order_col} DESC LIMIT ?",
|
|
265
|
-
(limit,),
|
|
266
|
-
)
|
|
267
|
-
rows = cursor.fetchall()
|
|
268
|
-
# Convert to list of dicts
|
|
269
|
-
result = []
|
|
270
|
-
for row in rows:
|
|
271
|
-
d = {}
|
|
272
|
-
for i, col in enumerate(select_cols):
|
|
273
|
-
d[col] = row[i]
|
|
274
|
-
result.append(d)
|
|
275
|
-
return result
|
|
276
|
-
finally:
|
|
277
|
-
conn.close()
|
|
278
|
-
except sqlite3.Error as e:
|
|
279
|
-
logger.warning("Failed to read recent memories: %s", e)
|
|
280
|
-
return []
|
|
281
|
-
|
|
282
|
-
def _get_available_columns(self) -> set:
|
|
283
|
-
"""
|
|
284
|
-
Get the set of column names in the memories table.
|
|
285
|
-
|
|
286
|
-
Cached after first call to avoid repeated PRAGMA queries.
|
|
287
|
-
"""
|
|
288
|
-
if self._available_columns is not None:
|
|
289
|
-
return self._available_columns
|
|
290
|
-
|
|
291
|
-
if not self._memory_db_path.exists():
|
|
292
|
-
return set()
|
|
293
|
-
|
|
294
|
-
try:
|
|
295
|
-
conn = self._open_memory_db()
|
|
296
|
-
try:
|
|
297
|
-
cursor = conn.cursor()
|
|
298
|
-
cursor.execute("PRAGMA table_info(memories)")
|
|
299
|
-
cols = {row[1] for row in cursor.fetchall()}
|
|
300
|
-
self._available_columns = cols
|
|
301
|
-
return cols
|
|
302
|
-
finally:
|
|
303
|
-
conn.close()
|
|
304
|
-
except sqlite3.Error as e:
|
|
305
|
-
logger.warning("Failed to read table schema: %s", e)
|
|
306
|
-
return set()
|
|
307
|
-
|
|
308
|
-
def _open_memory_db(self) -> sqlite3.Connection:
|
|
309
|
-
"""
|
|
310
|
-
Open a read-only connection to memory.db.
|
|
311
|
-
|
|
312
|
-
Uses uri=True with mode=ro to enforce read-only access.
|
|
313
|
-
Falls back to regular connection if URI mode fails
|
|
314
|
-
(some older Python builds do not support it).
|
|
315
|
-
"""
|
|
316
|
-
db_str = str(self._memory_db_path)
|
|
317
|
-
try:
|
|
318
|
-
# Prefer URI-based read-only mode
|
|
319
|
-
uri = f"file:{db_str}?mode=ro"
|
|
320
|
-
conn = sqlite3.connect(uri, uri=True, timeout=5)
|
|
321
|
-
except (sqlite3.OperationalError, sqlite3.NotSupportedError):
|
|
322
|
-
# Fallback: regular connection (still only reads)
|
|
323
|
-
conn = sqlite3.connect(db_str, timeout=5)
|
|
324
|
-
conn.execute("PRAGMA busy_timeout=3000")
|
|
325
|
-
return conn
|
|
326
|
-
|
|
327
|
-
# ------------------------------------------------------------------
|
|
328
|
-
# Signal extraction helpers
|
|
329
|
-
# ------------------------------------------------------------------
|
|
330
|
-
|
|
331
|
-
@staticmethod
|
|
332
|
-
def _extract_project_from_path(path: str) -> Optional[str]:
|
|
333
|
-
"""
|
|
334
|
-
Extract a project name from a file path.
|
|
335
|
-
|
|
336
|
-
Strategy:
|
|
337
|
-
1. Walk path parts looking for a directory that follows a
|
|
338
|
-
known parent directory (projects/, repos/, Documents/, etc.).
|
|
339
|
-
2. If found, the directory immediately after the parent is the
|
|
340
|
-
project name.
|
|
341
|
-
3. Fallback: use the last non-skip directory component.
|
|
342
|
-
|
|
343
|
-
Examples:
|
|
344
|
-
/Users/x/projects/MY_PROJECT/src/main.py -> "MY_PROJECT"
|
|
345
|
-
/home/x/repos/my-app/lib/util.js -> "my-app"
|
|
346
|
-
/workspace/services/auth-service/index.ts -> "auth-service"
|
|
347
|
-
|
|
348
|
-
Returns:
|
|
349
|
-
Project name string or None if extraction fails.
|
|
350
|
-
"""
|
|
351
|
-
if not path:
|
|
352
|
-
return None
|
|
353
|
-
|
|
354
|
-
try:
|
|
355
|
-
parts = Path(path).parts
|
|
356
|
-
except (ValueError, TypeError):
|
|
357
|
-
return None
|
|
358
|
-
|
|
359
|
-
if len(parts) < 2:
|
|
360
|
-
return None
|
|
361
|
-
|
|
362
|
-
# Strategy 1: find part after a known parent directory.
|
|
363
|
-
# Skip consecutive parent dirs (e.g., workspace/services/ both
|
|
364
|
-
# are parent dirs, so the project is the NEXT non-parent part).
|
|
365
|
-
for i, part in enumerate(parts):
|
|
366
|
-
if part in _PROJECT_PARENT_DIRS:
|
|
367
|
-
# Walk forward past any chained parent dirs
|
|
368
|
-
j = i + 1
|
|
369
|
-
while j < len(parts) and parts[j] in _PROJECT_PARENT_DIRS:
|
|
370
|
-
j += 1
|
|
371
|
-
if j < len(parts):
|
|
372
|
-
candidate = parts[j]
|
|
373
|
-
if (
|
|
374
|
-
candidate
|
|
375
|
-
and candidate not in _SKIP_DIRS
|
|
376
|
-
and not candidate.startswith('.')
|
|
377
|
-
):
|
|
378
|
-
return candidate
|
|
379
|
-
|
|
380
|
-
# Strategy 2: walk backwards to find last meaningful directory
|
|
381
|
-
# Skip leaf (likely a filename) and known non-project dirs
|
|
382
|
-
for part in reversed(parts[:-1]): # exclude the last component (filename)
|
|
383
|
-
if (
|
|
384
|
-
part
|
|
385
|
-
and part not in _SKIP_DIRS
|
|
386
|
-
and part not in _PROJECT_PARENT_DIRS
|
|
387
|
-
and not part.startswith('.')
|
|
388
|
-
and not part.startswith('/')
|
|
389
|
-
and len(part) > 1
|
|
390
|
-
):
|
|
391
|
-
return part
|
|
392
|
-
|
|
393
|
-
return None
|
|
394
|
-
|
|
395
|
-
@staticmethod
|
|
396
|
-
def _get_active_profile() -> Optional[str]:
|
|
397
|
-
"""
|
|
398
|
-
Read the active profile name from profiles.json.
|
|
399
|
-
|
|
400
|
-
Returns:
|
|
401
|
-
Profile name string (e.g., "work", "personal") or None.
|
|
402
|
-
"""
|
|
403
|
-
if not PROFILES_JSON.exists():
|
|
404
|
-
return None
|
|
405
|
-
|
|
406
|
-
try:
|
|
407
|
-
with open(PROFILES_JSON, 'r') as f:
|
|
408
|
-
config = json.load(f)
|
|
409
|
-
return config.get('active_profile', 'default')
|
|
410
|
-
except (json.JSONDecodeError, OSError, KeyError) as e:
|
|
411
|
-
logger.debug("Failed to read profiles.json: %s", e)
|
|
412
|
-
return None
|
|
413
|
-
|
|
414
|
-
def _match_content_to_clusters(
|
|
415
|
-
self,
|
|
416
|
-
recent_memories: List[Dict[str, Any]],
|
|
417
|
-
) -> Optional[str]:
|
|
418
|
-
"""
|
|
419
|
-
Check if recent memories converge on a single cluster.
|
|
420
|
-
|
|
421
|
-
If the most recent 10 memories share a dominant cluster_id, look
|
|
422
|
-
up that cluster's name in graph_clusters and cross-reference with
|
|
423
|
-
the most common project_name within that cluster.
|
|
424
|
-
|
|
425
|
-
Returns:
|
|
426
|
-
A project name inferred from cluster dominance, or None.
|
|
427
|
-
"""
|
|
428
|
-
# Collect cluster_ids from the most recent 10 memories
|
|
429
|
-
cluster_ids = []
|
|
430
|
-
for mem in recent_memories[:10]:
|
|
431
|
-
cid = self._safe_get(mem, 'cluster_id')
|
|
432
|
-
if cid is not None:
|
|
433
|
-
cluster_ids.append(cid)
|
|
434
|
-
|
|
435
|
-
if not cluster_ids:
|
|
436
|
-
return None
|
|
437
|
-
|
|
438
|
-
# Find dominant cluster
|
|
439
|
-
cluster_counts = Counter(cluster_ids)
|
|
440
|
-
dominant_id, dominant_count = cluster_counts.most_common(1)[0]
|
|
441
|
-
|
|
442
|
-
# Require at least 40% dominance (at least 4 out of 10)
|
|
443
|
-
if dominant_count < max(2, len(cluster_ids) * 0.4):
|
|
444
|
-
return None
|
|
445
|
-
|
|
446
|
-
# Look up the dominant project_name within that cluster
|
|
447
|
-
return self._get_cluster_dominant_project(dominant_id)
|
|
448
|
-
|
|
449
|
-
def _get_cluster_dominant_project(self, cluster_id: int) -> Optional[str]:
|
|
450
|
-
"""
|
|
451
|
-
Find the most common project_name among memories in a given cluster.
|
|
452
|
-
|
|
453
|
-
Falls back to the cluster name from graph_clusters if no explicit
|
|
454
|
-
project_name is found.
|
|
455
|
-
"""
|
|
456
|
-
if not self._memory_db_path.exists():
|
|
457
|
-
return None
|
|
458
|
-
|
|
459
|
-
available = self._get_available_columns()
|
|
460
|
-
|
|
461
|
-
try:
|
|
462
|
-
conn = self._open_memory_db()
|
|
463
|
-
try:
|
|
464
|
-
cursor = conn.cursor()
|
|
465
|
-
|
|
466
|
-
# Try to find the most common project_name in this cluster
|
|
467
|
-
if 'project_name' in available and 'cluster_id' in available:
|
|
468
|
-
cursor.execute(
|
|
469
|
-
"SELECT project_name, COUNT(*) as cnt "
|
|
470
|
-
"FROM memories "
|
|
471
|
-
"WHERE cluster_id = ? AND project_name IS NOT NULL "
|
|
472
|
-
"AND project_name != '' "
|
|
473
|
-
"GROUP BY project_name "
|
|
474
|
-
"ORDER BY cnt DESC LIMIT 1",
|
|
475
|
-
(cluster_id,),
|
|
476
|
-
)
|
|
477
|
-
row = cursor.fetchone()
|
|
478
|
-
if row and row[0]:
|
|
479
|
-
return row[0]
|
|
480
|
-
|
|
481
|
-
# Fallback: use the cluster name from graph_clusters
|
|
482
|
-
try:
|
|
483
|
-
cursor.execute(
|
|
484
|
-
"SELECT name FROM graph_clusters WHERE id = ?",
|
|
485
|
-
(cluster_id,),
|
|
486
|
-
)
|
|
487
|
-
row = cursor.fetchone()
|
|
488
|
-
if row and row[0]:
|
|
489
|
-
return row[0]
|
|
490
|
-
except sqlite3.OperationalError:
|
|
491
|
-
# graph_clusters table may not exist
|
|
492
|
-
pass
|
|
493
|
-
|
|
494
|
-
return None
|
|
495
|
-
finally:
|
|
496
|
-
conn.close()
|
|
497
|
-
except sqlite3.Error as e:
|
|
498
|
-
logger.debug(
|
|
499
|
-
"Failed to query cluster %d project: %s", cluster_id, e
|
|
500
|
-
)
|
|
501
|
-
return None
|
|
502
|
-
|
|
503
|
-
# ------------------------------------------------------------------
|
|
504
|
-
# Utilities
|
|
505
|
-
# ------------------------------------------------------------------
|
|
506
|
-
|
|
507
|
-
@staticmethod
|
|
508
|
-
def _safe_get(d: Dict[str, Any], key: str) -> Any:
|
|
509
|
-
"""
|
|
510
|
-
Safely get a value from a dict, returning None for missing keys
|
|
511
|
-
or empty/whitespace-only strings.
|
|
512
|
-
"""
|
|
513
|
-
val = d.get(key)
|
|
514
|
-
if val is None:
|
|
515
|
-
return None
|
|
516
|
-
if isinstance(val, str) and not val.strip():
|
|
517
|
-
return None
|
|
518
|
-
return val
|
|
519
|
-
|
|
520
|
-
def invalidate_cache(self):
|
|
521
|
-
"""
|
|
522
|
-
Clear the cached column set.
|
|
523
|
-
|
|
524
|
-
Call this if the memory.db schema may have changed at runtime
|
|
525
|
-
(e.g., after a migration adds new columns).
|
|
526
|
-
"""
|
|
527
|
-
self._available_columns = None
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
# ======================================================================
|
|
531
|
-
# Standalone testing
|
|
532
|
-
# ======================================================================
|
|
533
|
-
|
|
534
|
-
if __name__ == "__main__":
|
|
535
|
-
logging.basicConfig(
|
|
536
|
-
level=logging.DEBUG,
|
|
537
|
-
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
pcm = ProjectContextManager()
|
|
541
|
-
|
|
542
|
-
# Test path extraction
|
|
543
|
-
test_paths = [
|
|
544
|
-
"/Users/varun/projects/SuperLocalMemoryV2/src/main.py",
|
|
545
|
-
"/home/dev/repos/my-app/lib/util.js",
|
|
546
|
-
"/workspace/services/auth-service/index.ts",
|
|
547
|
-
"/Users/varun/Documents/AGENTIC_Official/SuperLocalMemoryV2-repo/src/learning/foo.py",
|
|
548
|
-
"",
|
|
549
|
-
None,
|
|
550
|
-
]
|
|
551
|
-
print("=== Path Extraction Tests ===")
|
|
552
|
-
for p in test_paths:
|
|
553
|
-
result = ProjectContextManager._extract_project_from_path(p)
|
|
554
|
-
print(f" {p!r:60s} -> {result!r}")
|
|
555
|
-
|
|
556
|
-
# Test full detection
|
|
557
|
-
print("\n=== Project Detection ===")
|
|
558
|
-
project = pcm.detect_current_project()
|
|
559
|
-
print(f" Detected project: {project!r}")
|
|
560
|
-
|
|
561
|
-
# Test boost
|
|
562
|
-
print("\n=== Boost Tests ===")
|
|
563
|
-
if project:
|
|
564
|
-
test_mem_match = {'project_name': project}
|
|
565
|
-
test_mem_miss = {'project_name': 'other-project'}
|
|
566
|
-
test_mem_none = {'content': 'no project info'}
|
|
567
|
-
print(f" Match boost: {pcm.get_project_boost(test_mem_match, project)}")
|
|
568
|
-
print(f" Mismatch boost: {pcm.get_project_boost(test_mem_miss, project)}")
|
|
569
|
-
print(f" Unknown boost: {pcm.get_project_boost(test_mem_none, project)}")
|
|
570
|
-
else:
|
|
571
|
-
print(" No project detected — all boosts return 0.6 (neutral)")
|
|
572
|
-
print(f" Neutral boost: {pcm.get_project_boost({}, None)}")
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
-
"""
|
|
5
|
-
Ranking utilities package for AdaptiveRanker.
|
|
6
|
-
|
|
7
|
-
Provides constants, helpers, and feature utilities extracted from
|
|
8
|
-
the main adaptive_ranker.py module for better maintainability.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from .constants import (
|
|
12
|
-
MODELS_DIR,
|
|
13
|
-
MODEL_PATH,
|
|
14
|
-
PHASE_THRESHOLDS,
|
|
15
|
-
MIN_UNIQUE_QUERIES_FOR_ML,
|
|
16
|
-
RULE_BOOST,
|
|
17
|
-
TRAINING_PARAMS,
|
|
18
|
-
)
|
|
19
|
-
from .helpers import (
|
|
20
|
-
calculate_rule_boost,
|
|
21
|
-
prepare_training_data_internal,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
__all__ = [
|
|
25
|
-
'MODELS_DIR',
|
|
26
|
-
'MODEL_PATH',
|
|
27
|
-
'PHASE_THRESHOLDS',
|
|
28
|
-
'MIN_UNIQUE_QUERIES_FOR_ML',
|
|
29
|
-
'RULE_BOOST',
|
|
30
|
-
'TRAINING_PARAMS',
|
|
31
|
-
'calculate_rule_boost',
|
|
32
|
-
'prepare_training_data_internal',
|
|
33
|
-
]
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
-
"""
|
|
5
|
-
Constants for AdaptiveRanker.
|
|
6
|
-
|
|
7
|
-
Includes phase thresholds, rule-based boost multipliers, and LightGBM
|
|
8
|
-
training parameters.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
# ============================================================================
|
|
14
|
-
# Paths
|
|
15
|
-
# ============================================================================
|
|
16
|
-
|
|
17
|
-
MODELS_DIR = Path.home() / ".claude-memory" / "models"
|
|
18
|
-
MODEL_PATH = MODELS_DIR / "ranker.txt"
|
|
19
|
-
|
|
20
|
-
# ============================================================================
|
|
21
|
-
# Phase Thresholds
|
|
22
|
-
# ============================================================================
|
|
23
|
-
|
|
24
|
-
# Phase thresholds — how many feedback signals to trigger each phase
|
|
25
|
-
PHASE_THRESHOLDS = {
|
|
26
|
-
'baseline': 0, # 0 feedback samples -> no re-ranking
|
|
27
|
-
'rule_based': 20, # 20+ feedback -> rule-based boosting
|
|
28
|
-
'ml_model': 200, # 200+ feedback across 50+ unique queries -> ML
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
# Minimum unique queries required for ML phase (prevents overfitting
|
|
32
|
-
# to a small number of repeated queries)
|
|
33
|
-
MIN_UNIQUE_QUERIES_FOR_ML = 50
|
|
34
|
-
|
|
35
|
-
# ============================================================================
|
|
36
|
-
# Rule-Based Boost Multipliers (Phase 1)
|
|
37
|
-
# ============================================================================
|
|
38
|
-
|
|
39
|
-
# These are conservative — they nudge the ranking without flipping order
|
|
40
|
-
RULE_BOOST = {
|
|
41
|
-
'tech_match_strong': 1.3, # Memory matches 2+ preferred techs
|
|
42
|
-
'tech_match_weak': 1.1, # Memory matches 1 preferred tech
|
|
43
|
-
'project_match': 1.5, # Memory from current project
|
|
44
|
-
'project_unknown': 1.0, # No project context — no boost
|
|
45
|
-
'project_mismatch': 0.9, # Memory from different project
|
|
46
|
-
'source_quality_high': 1.2, # Source quality > 0.7
|
|
47
|
-
'source_quality_low': 0.85, # Source quality < 0.3
|
|
48
|
-
'recency_boost_max': 1.2, # Recent memory (< 7 days)
|
|
49
|
-
'recency_penalty_max': 0.8, # Old memory (> 365 days)
|
|
50
|
-
'high_importance': 1.15, # Importance >= 8
|
|
51
|
-
'high_access': 1.1, # Accessed 5+ times
|
|
52
|
-
# v2.8: Lifecycle + behavioral boosts
|
|
53
|
-
'lifecycle_active': 1.0,
|
|
54
|
-
'lifecycle_warm': 0.85,
|
|
55
|
-
'lifecycle_cold': 0.6,
|
|
56
|
-
'outcome_success_high': 1.3,
|
|
57
|
-
'outcome_failure_high': 0.7,
|
|
58
|
-
'behavioral_match_strong': 1.25,
|
|
59
|
-
'cross_project_boost': 1.15,
|
|
60
|
-
'high_trust_creator': 1.1,
|
|
61
|
-
'low_trust_creator': 0.8,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# ============================================================================
|
|
65
|
-
# LightGBM Training Parameters
|
|
66
|
-
# ============================================================================
|
|
67
|
-
|
|
68
|
-
# LightGBM training parameters — tuned for small, personal datasets
|
|
69
|
-
# Aggressive regularization prevents overfitting on < 10K samples
|
|
70
|
-
TRAINING_PARAMS = {
|
|
71
|
-
'objective': 'lambdarank',
|
|
72
|
-
'metric': 'ndcg',
|
|
73
|
-
'ndcg_eval_at': [5, 10],
|
|
74
|
-
'learning_rate': 0.05,
|
|
75
|
-
'num_leaves': 16,
|
|
76
|
-
'max_depth': 4,
|
|
77
|
-
'min_child_samples': 10,
|
|
78
|
-
'subsample': 0.8,
|
|
79
|
-
'reg_alpha': 0.1,
|
|
80
|
-
'reg_lambda': 1.0,
|
|
81
|
-
'boosting_type': 'dart',
|
|
82
|
-
'n_estimators': 50,
|
|
83
|
-
'verbose': -1,
|
|
84
|
-
}
|