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.
Files changed (431) hide show
  1. package/LICENSE +9 -1
  2. package/NOTICE +63 -0
  3. package/README.md +165 -480
  4. package/bin/slm +17 -449
  5. package/bin/slm-npm +1 -1
  6. package/conftest.py +5 -0
  7. package/docs/api-reference.md +284 -0
  8. package/docs/architecture.md +149 -0
  9. package/docs/auto-memory.md +150 -0
  10. package/docs/cli-reference.md +276 -0
  11. package/docs/compliance.md +191 -0
  12. package/docs/configuration.md +182 -0
  13. package/docs/getting-started.md +102 -0
  14. package/docs/ide-setup.md +261 -0
  15. package/docs/mcp-tools.md +220 -0
  16. package/docs/migration-from-v2.md +170 -0
  17. package/docs/profiles.md +173 -0
  18. package/docs/troubleshooting.md +310 -0
  19. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  20. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  21. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  22. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  23. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  24. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  25. package/ide/configs/cursor-mcp.json +15 -0
  26. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  27. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  28. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  29. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  30. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  31. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  32. package/{configs → ide/configs}/zed-mcp.json +2 -2
  33. package/{hooks → ide/hooks}/context-hook.js +9 -20
  34. package/ide/hooks/memory-list-skill.js +70 -0
  35. package/ide/hooks/memory-profile-skill.js +101 -0
  36. package/ide/hooks/memory-recall-skill.js +62 -0
  37. package/ide/hooks/memory-remember-skill.js +68 -0
  38. package/ide/hooks/memory-reset-skill.js +160 -0
  39. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  40. package/ide/integrations/langchain/README.md +106 -0
  41. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  42. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  43. package/ide/integrations/langchain/pyproject.toml +38 -0
  44. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  45. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  46. package/ide/integrations/langchain/tests/test_security.py +117 -0
  47. package/ide/integrations/llamaindex/README.md +81 -0
  48. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  49. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  50. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  51. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  52. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  53. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  54. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  55. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  56. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  57. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  58. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  59. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  60. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  61. package/package.json +13 -22
  62. package/pyproject.toml +85 -0
  63. package/scripts/build-dmg.sh +417 -0
  64. package/scripts/install-skills.ps1 +334 -0
  65. package/scripts/postinstall.js +2 -2
  66. package/scripts/start-dashboard.ps1 +52 -0
  67. package/scripts/start-dashboard.sh +41 -0
  68. package/scripts/sync-wiki.ps1 +127 -0
  69. package/scripts/sync-wiki.sh +82 -0
  70. package/scripts/test-dmg.sh +161 -0
  71. package/scripts/test-npm-package.ps1 +252 -0
  72. package/scripts/test-npm-package.sh +207 -0
  73. package/scripts/verify-install.ps1 +294 -0
  74. package/scripts/verify-install.sh +266 -0
  75. package/src/superlocalmemory/__init__.py +0 -0
  76. package/src/superlocalmemory/attribution/__init__.py +9 -0
  77. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  78. package/src/superlocalmemory/attribution/signer.py +153 -0
  79. package/src/superlocalmemory/attribution/watermark.py +189 -0
  80. package/src/superlocalmemory/cli/__init__.py +5 -0
  81. package/src/superlocalmemory/cli/commands.py +245 -0
  82. package/src/superlocalmemory/cli/main.py +89 -0
  83. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  84. package/src/superlocalmemory/cli/post_install.py +99 -0
  85. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  86. package/src/superlocalmemory/compliance/__init__.py +0 -0
  87. package/src/superlocalmemory/compliance/abac.py +204 -0
  88. package/src/superlocalmemory/compliance/audit.py +314 -0
  89. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  90. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  91. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  92. package/src/superlocalmemory/compliance/retention.py +232 -0
  93. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  94. package/src/superlocalmemory/core/__init__.py +0 -0
  95. package/src/superlocalmemory/core/config.py +391 -0
  96. package/src/superlocalmemory/core/embeddings.py +293 -0
  97. package/src/superlocalmemory/core/engine.py +701 -0
  98. package/src/superlocalmemory/core/hooks.py +65 -0
  99. package/src/superlocalmemory/core/maintenance.py +172 -0
  100. package/src/superlocalmemory/core/modes.py +140 -0
  101. package/src/superlocalmemory/core/profiles.py +234 -0
  102. package/src/superlocalmemory/core/registry.py +117 -0
  103. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  104. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  105. package/src/superlocalmemory/encoding/__init__.py +0 -0
  106. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  107. package/src/superlocalmemory/encoding/emotional.py +125 -0
  108. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  109. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  110. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  111. package/src/superlocalmemory/encoding/foresight.py +91 -0
  112. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  113. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  114. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  115. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  116. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  117. package/src/superlocalmemory/encoding/type_router.py +235 -0
  118. package/src/superlocalmemory/hooks/__init__.py +3 -0
  119. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  120. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  121. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  122. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  123. package/src/superlocalmemory/infra/__init__.py +3 -0
  124. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  125. package/src/superlocalmemory/infra/backup.py +317 -0
  126. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  127. package/src/superlocalmemory/infra/event_bus.py +381 -0
  128. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  129. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  130. package/src/superlocalmemory/learning/__init__.py +0 -0
  131. package/src/superlocalmemory/learning/adaptive.py +172 -0
  132. package/src/superlocalmemory/learning/behavioral.py +490 -0
  133. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  134. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  135. package/src/superlocalmemory/learning/cross_project.py +399 -0
  136. package/src/superlocalmemory/learning/database.py +376 -0
  137. package/src/superlocalmemory/learning/engagement.py +323 -0
  138. package/src/superlocalmemory/learning/features.py +138 -0
  139. package/src/superlocalmemory/learning/feedback.py +316 -0
  140. package/src/superlocalmemory/learning/outcomes.py +255 -0
  141. package/src/superlocalmemory/learning/project_context.py +366 -0
  142. package/src/superlocalmemory/learning/ranker.py +155 -0
  143. package/src/superlocalmemory/learning/source_quality.py +303 -0
  144. package/src/superlocalmemory/learning/workflows.py +309 -0
  145. package/src/superlocalmemory/llm/__init__.py +0 -0
  146. package/src/superlocalmemory/llm/backbone.py +316 -0
  147. package/src/superlocalmemory/math/__init__.py +0 -0
  148. package/src/superlocalmemory/math/fisher.py +356 -0
  149. package/src/superlocalmemory/math/langevin.py +398 -0
  150. package/src/superlocalmemory/math/sheaf.py +257 -0
  151. package/src/superlocalmemory/mcp/__init__.py +0 -0
  152. package/src/superlocalmemory/mcp/resources.py +245 -0
  153. package/src/superlocalmemory/mcp/server.py +61 -0
  154. package/src/superlocalmemory/mcp/tools.py +18 -0
  155. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  156. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  157. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  158. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  159. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  160. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  161. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  162. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  163. package/src/superlocalmemory/retrieval/engine.py +390 -0
  164. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  165. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  166. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  167. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  168. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  169. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  170. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  171. package/src/superlocalmemory/server/__init__.py +1 -0
  172. package/src/superlocalmemory/server/api.py +248 -0
  173. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  174. package/src/superlocalmemory/server/routes/agents.py +107 -0
  175. package/src/superlocalmemory/server/routes/backup.py +91 -0
  176. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  177. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  178. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  179. package/src/superlocalmemory/server/routes/events.py +183 -0
  180. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  181. package/src/superlocalmemory/server/routes/learning.py +273 -0
  182. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  183. package/src/superlocalmemory/server/routes/memories.py +399 -0
  184. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  185. package/src/superlocalmemory/server/routes/stats.py +346 -0
  186. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  187. package/src/superlocalmemory/server/routes/ws.py +82 -0
  188. package/src/superlocalmemory/server/security_middleware.py +57 -0
  189. package/src/superlocalmemory/server/ui.py +245 -0
  190. package/src/superlocalmemory/storage/__init__.py +0 -0
  191. package/src/superlocalmemory/storage/access_control.py +182 -0
  192. package/src/superlocalmemory/storage/database.py +594 -0
  193. package/src/superlocalmemory/storage/migrations.py +303 -0
  194. package/src/superlocalmemory/storage/models.py +406 -0
  195. package/src/superlocalmemory/storage/schema.py +726 -0
  196. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  197. package/src/superlocalmemory/trust/__init__.py +0 -0
  198. package/src/superlocalmemory/trust/gate.py +130 -0
  199. package/src/superlocalmemory/trust/provenance.py +124 -0
  200. package/src/superlocalmemory/trust/scorer.py +347 -0
  201. package/src/superlocalmemory/trust/signals.py +153 -0
  202. package/ui/index.html +278 -5
  203. package/ui/js/auto-settings.js +70 -0
  204. package/ui/js/dashboard.js +90 -0
  205. package/ui/js/fact-detail.js +92 -0
  206. package/ui/js/feedback.js +2 -2
  207. package/ui/js/ide-status.js +102 -0
  208. package/ui/js/math-health.js +98 -0
  209. package/ui/js/recall-lab.js +127 -0
  210. package/ui/js/settings.js +2 -2
  211. package/ui/js/trust-dashboard.js +73 -0
  212. package/api_server.py +0 -724
  213. package/bin/aider-smart +0 -72
  214. package/bin/superlocalmemoryv2-learning +0 -4
  215. package/bin/superlocalmemoryv2-list +0 -3
  216. package/bin/superlocalmemoryv2-patterns +0 -4
  217. package/bin/superlocalmemoryv2-profile +0 -3
  218. package/bin/superlocalmemoryv2-recall +0 -3
  219. package/bin/superlocalmemoryv2-remember +0 -3
  220. package/bin/superlocalmemoryv2-reset +0 -3
  221. package/bin/superlocalmemoryv2-status +0 -3
  222. package/configs/chatgpt-desktop-mcp.json +0 -16
  223. package/configs/cursor-mcp.json +0 -15
  224. package/hooks/memory-list-skill.js +0 -139
  225. package/hooks/memory-profile-skill.js +0 -273
  226. package/hooks/memory-recall-skill.js +0 -114
  227. package/hooks/memory-remember-skill.js +0 -127
  228. package/hooks/memory-reset-skill.js +0 -274
  229. package/mcp_server.py +0 -1808
  230. package/requirements-core.txt +0 -22
  231. package/requirements-learning.txt +0 -12
  232. package/requirements.txt +0 -12
  233. package/src/agent_registry.py +0 -411
  234. package/src/auth_middleware.py +0 -61
  235. package/src/auto_backup.py +0 -459
  236. package/src/behavioral/__init__.py +0 -49
  237. package/src/behavioral/behavioral_listener.py +0 -203
  238. package/src/behavioral/behavioral_patterns.py +0 -275
  239. package/src/behavioral/cross_project_transfer.py +0 -206
  240. package/src/behavioral/outcome_inference.py +0 -194
  241. package/src/behavioral/outcome_tracker.py +0 -193
  242. package/src/behavioral/tests/__init__.py +0 -4
  243. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  244. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  245. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  246. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  247. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  248. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  249. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  250. package/src/cache_manager.py +0 -518
  251. package/src/compliance/__init__.py +0 -48
  252. package/src/compliance/abac_engine.py +0 -149
  253. package/src/compliance/abac_middleware.py +0 -116
  254. package/src/compliance/audit_db.py +0 -215
  255. package/src/compliance/audit_logger.py +0 -148
  256. package/src/compliance/retention_manager.py +0 -289
  257. package/src/compliance/retention_scheduler.py +0 -186
  258. package/src/compliance/tests/__init__.py +0 -4
  259. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  260. package/src/compliance/tests/test_abac_engine.py +0 -124
  261. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  262. package/src/compliance/tests/test_audit_db.py +0 -123
  263. package/src/compliance/tests/test_audit_logger.py +0 -98
  264. package/src/compliance/tests/test_mcp_audit.py +0 -128
  265. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  266. package/src/compliance/tests/test_retention_manager.py +0 -131
  267. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  268. package/src/compression/__init__.py +0 -25
  269. package/src/compression/cli.py +0 -150
  270. package/src/compression/cold_storage.py +0 -217
  271. package/src/compression/config.py +0 -72
  272. package/src/compression/orchestrator.py +0 -133
  273. package/src/compression/tier2_compressor.py +0 -228
  274. package/src/compression/tier3_compressor.py +0 -153
  275. package/src/compression/tier_classifier.py +0 -148
  276. package/src/db_connection_manager.py +0 -536
  277. package/src/embedding_engine.py +0 -63
  278. package/src/embeddings/__init__.py +0 -47
  279. package/src/embeddings/cache.py +0 -70
  280. package/src/embeddings/cli.py +0 -113
  281. package/src/embeddings/constants.py +0 -47
  282. package/src/embeddings/database.py +0 -91
  283. package/src/embeddings/engine.py +0 -247
  284. package/src/embeddings/model_loader.py +0 -145
  285. package/src/event_bus.py +0 -562
  286. package/src/graph/__init__.py +0 -36
  287. package/src/graph/build_helpers.py +0 -74
  288. package/src/graph/cli.py +0 -87
  289. package/src/graph/cluster_builder.py +0 -188
  290. package/src/graph/cluster_summary.py +0 -148
  291. package/src/graph/constants.py +0 -47
  292. package/src/graph/edge_builder.py +0 -162
  293. package/src/graph/entity_extractor.py +0 -95
  294. package/src/graph/graph_core.py +0 -226
  295. package/src/graph/graph_search.py +0 -231
  296. package/src/graph/hierarchical.py +0 -207
  297. package/src/graph/schema.py +0 -99
  298. package/src/graph_engine.py +0 -52
  299. package/src/hnsw_index.py +0 -628
  300. package/src/hybrid_search.py +0 -46
  301. package/src/learning/__init__.py +0 -217
  302. package/src/learning/adaptive_ranker.py +0 -682
  303. package/src/learning/bootstrap/__init__.py +0 -69
  304. package/src/learning/bootstrap/constants.py +0 -93
  305. package/src/learning/bootstrap/db_queries.py +0 -316
  306. package/src/learning/bootstrap/sampling.py +0 -82
  307. package/src/learning/bootstrap/text_utils.py +0 -71
  308. package/src/learning/cross_project_aggregator.py +0 -857
  309. package/src/learning/db/__init__.py +0 -40
  310. package/src/learning/db/constants.py +0 -44
  311. package/src/learning/db/schema.py +0 -279
  312. package/src/learning/engagement_tracker.py +0 -628
  313. package/src/learning/feature_extractor.py +0 -708
  314. package/src/learning/feedback_collector.py +0 -806
  315. package/src/learning/learning_db.py +0 -915
  316. package/src/learning/project_context_manager.py +0 -572
  317. package/src/learning/ranking/__init__.py +0 -33
  318. package/src/learning/ranking/constants.py +0 -84
  319. package/src/learning/ranking/helpers.py +0 -278
  320. package/src/learning/source_quality_scorer.py +0 -676
  321. package/src/learning/synthetic_bootstrap.py +0 -755
  322. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  323. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  324. package/src/learning/tests/test_aggregator.py +0 -306
  325. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  326. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  327. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  328. package/src/learning/tests/test_feedback_collector.py +0 -294
  329. package/src/learning/tests/test_learning_db.py +0 -602
  330. package/src/learning/tests/test_learning_db_v28.py +0 -110
  331. package/src/learning/tests/test_learning_init_v28.py +0 -48
  332. package/src/learning/tests/test_outcome_signals.py +0 -48
  333. package/src/learning/tests/test_project_context.py +0 -292
  334. package/src/learning/tests/test_schema_migration.py +0 -319
  335. package/src/learning/tests/test_signal_inference.py +0 -397
  336. package/src/learning/tests/test_source_quality.py +0 -351
  337. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  338. package/src/learning/tests/test_workflow_miner.py +0 -318
  339. package/src/learning/workflow_pattern_miner.py +0 -655
  340. package/src/lifecycle/__init__.py +0 -54
  341. package/src/lifecycle/bounded_growth.py +0 -239
  342. package/src/lifecycle/compaction_engine.py +0 -226
  343. package/src/lifecycle/lifecycle_engine.py +0 -355
  344. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  345. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  346. package/src/lifecycle/retention_policy.py +0 -285
  347. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  348. package/src/lifecycle/tests/test_compaction.py +0 -179
  349. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  350. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  351. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  352. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  353. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  354. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  355. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  356. package/src/mcp_tools_v28.py +0 -281
  357. package/src/memory/__init__.py +0 -36
  358. package/src/memory/cli.py +0 -205
  359. package/src/memory/constants.py +0 -39
  360. package/src/memory/helpers.py +0 -28
  361. package/src/memory/schema.py +0 -166
  362. package/src/memory-profiles.py +0 -595
  363. package/src/memory-reset.py +0 -491
  364. package/src/memory_compression.py +0 -989
  365. package/src/memory_store_v2.py +0 -1155
  366. package/src/migrate_v1_to_v2.py +0 -629
  367. package/src/pattern_learner.py +0 -34
  368. package/src/patterns/__init__.py +0 -24
  369. package/src/patterns/analyzers.py +0 -251
  370. package/src/patterns/learner.py +0 -271
  371. package/src/patterns/scoring.py +0 -171
  372. package/src/patterns/store.py +0 -225
  373. package/src/patterns/terminology.py +0 -140
  374. package/src/provenance_tracker.py +0 -312
  375. package/src/qualixar_attribution.py +0 -139
  376. package/src/qualixar_watermark.py +0 -78
  377. package/src/query_optimizer.py +0 -511
  378. package/src/rate_limiter.py +0 -83
  379. package/src/search/__init__.py +0 -20
  380. package/src/search/cli.py +0 -77
  381. package/src/search/constants.py +0 -26
  382. package/src/search/engine.py +0 -241
  383. package/src/search/fusion.py +0 -122
  384. package/src/search/index_loader.py +0 -114
  385. package/src/search/methods.py +0 -162
  386. package/src/search_engine_v2.py +0 -401
  387. package/src/setup_validator.py +0 -482
  388. package/src/subscription_manager.py +0 -391
  389. package/src/tree/__init__.py +0 -59
  390. package/src/tree/builder.py +0 -185
  391. package/src/tree/nodes.py +0 -202
  392. package/src/tree/queries.py +0 -257
  393. package/src/tree/schema.py +0 -80
  394. package/src/tree_manager.py +0 -19
  395. package/src/trust/__init__.py +0 -45
  396. package/src/trust/constants.py +0 -66
  397. package/src/trust/queries.py +0 -157
  398. package/src/trust/schema.py +0 -95
  399. package/src/trust/scorer.py +0 -299
  400. package/src/trust/signals.py +0 -95
  401. package/src/trust_scorer.py +0 -44
  402. package/ui/app.js +0 -1588
  403. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  404. package/ui/js/graph-cytoscape.js +0 -1168
  405. package/ui/js/graph-d3-backup.js +0 -32
  406. package/ui/js/graph.js +0 -32
  407. package/ui_server.py +0 -286
  408. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  409. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  410. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  411. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  412. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  413. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  414. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  415. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  416. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  417. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  418. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  419. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  420. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  421. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  422. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  423. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  424. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  425. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  426. /package/{completions → ide/completions}/slm.bash +0 -0
  427. /package/{completions → ide/completions}/slm.zsh +0 -0
  428. /package/{configs → ide/configs}/cody-commands.json +0 -0
  429. /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
  430. /package/{install.ps1 → scripts/install.ps1} +0 -0
  431. /package/{install.sh → scripts/install.sh} +0 -0
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """SuperLocalMemory V2 - LlamaIndex Chat Store Backend
5
+
6
+ Implements LlamaIndex's BaseChatStore backed by SuperLocalMemory V2's
7
+ local SQLite storage. All data stays on-device — zero cloud, zero telemetry.
8
+
9
+ Usage:
10
+ from llama_index.storage.chat_store.superlocalmemory import SuperLocalMemoryChatStore
11
+ from llama_index.core.memory import ChatMemoryBuffer
12
+
13
+ chat_store = SuperLocalMemoryChatStore()
14
+ memory = ChatMemoryBuffer.from_defaults(chat_store=chat_store, chat_store_key="user-123")
15
+ """
16
+ import hashlib
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from llama_index.core.base.llms.types import ChatMessage, MessageRole
23
+ from llama_index.core.storage.chat_store.base import BaseChatStore
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Locate and import SuperLocalMemory V2's MemoryStoreV2
27
+ # ---------------------------------------------------------------------------
28
+ _SLM_PATH = Path.home() / ".claude-memory"
29
+ if str(_SLM_PATH) not in sys.path:
30
+ sys.path.insert(0, str(_SLM_PATH))
31
+
32
+ # Also support the source tree layout (for development / tests that pass db_path)
33
+ _SLM_SRC_PATH = Path(__file__).resolve().parents[6] / "src"
34
+ if _SLM_SRC_PATH.exists() and str(_SLM_SRC_PATH) not in sys.path:
35
+ sys.path.insert(0, str(_SLM_SRC_PATH))
36
+
37
+ try:
38
+ from memory_store_v2 import MemoryStoreV2
39
+ except ImportError as exc:
40
+ raise ImportError(
41
+ "SuperLocalMemory V2 is not installed. "
42
+ "Run: curl -fsSL https://raw.githubusercontent.com/varun369/SuperLocalMemoryV2/main/install.sh | bash\n"
43
+ "Or visit: https://github.com/varun369/SuperLocalMemoryV2"
44
+ ) from exc
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Constants
49
+ # ---------------------------------------------------------------------------
50
+ # Tag format: "li:chat:<hash>" where hash is a 24-char SHA-256 prefix of the key.
51
+ # Total tag length = 8 + 24 = 32 chars, well under SLM's 50-char MAX_TAG_LENGTH.
52
+ # The full session key is stored inside the serialized content JSON so get_keys()
53
+ # can reconstruct the original key even when the tag is hash-based.
54
+ _TAG_PREFIX = "li:chat:"
55
+ _PROJECT_NAME = "llamaindex"
56
+ _IMPORTANCE = 3
57
+ _LIST_LIMIT = 10000 # Upper bound when listing all memories for filtering
58
+ _HASH_LEN = 24 # Characters of SHA-256 hex digest to use in tags
59
+
60
+
61
+ def _key_hash(key: str) -> str:
62
+ """Produce a deterministic short hash of a session key for use in SLM tags."""
63
+ return hashlib.sha256(key.encode("utf-8")).hexdigest()[:_HASH_LEN]
64
+
65
+
66
+ def _make_tag(key: str) -> str:
67
+ """Build the SLM tag for a chat session key.
68
+
69
+ Uses a hash of the key to guarantee the tag never exceeds SLM's
70
+ 50-character limit regardless of key length or content.
71
+ """
72
+ return f"{_TAG_PREFIX}{_key_hash(key)}"
73
+
74
+
75
+ def _serialize_message(key: str, message: ChatMessage) -> str:
76
+ """Serialize a ChatMessage to JSON string for SLM content storage.
77
+
78
+ The session *key* is embedded in the payload so that ``get_keys()`` can
79
+ reconstruct the original key from stored memories (the tag only contains
80
+ a hash).
81
+ """
82
+ return json.dumps(
83
+ {
84
+ "key": key,
85
+ "role": message.role.value,
86
+ "content": message.content or "",
87
+ "additional_kwargs": message.additional_kwargs,
88
+ },
89
+ ensure_ascii=False,
90
+ )
91
+
92
+
93
+ def _deserialize_message(content: str) -> Optional[ChatMessage]:
94
+ """Deserialize a JSON string back to a ChatMessage.
95
+
96
+ Returns None if the content is not valid chat message JSON.
97
+ """
98
+ try:
99
+ data = json.loads(content)
100
+ except (json.JSONDecodeError, TypeError):
101
+ return None
102
+
103
+ if not isinstance(data, dict) or "role" not in data:
104
+ return None
105
+
106
+ try:
107
+ role = MessageRole(data["role"])
108
+ except ValueError:
109
+ role = MessageRole.USER
110
+
111
+ return ChatMessage(
112
+ role=role,
113
+ content=data.get("content", ""),
114
+ additional_kwargs=data.get("additional_kwargs", {}),
115
+ )
116
+
117
+
118
+ def _extract_key(content: str) -> Optional[str]:
119
+ """Extract the session key from a serialized memory content string."""
120
+ try:
121
+ data = json.loads(content)
122
+ except (json.JSONDecodeError, TypeError):
123
+ return None
124
+ if isinstance(data, dict):
125
+ return data.get("key")
126
+ return None
127
+
128
+
129
+ class SuperLocalMemoryChatStore(BaseChatStore):
130
+ """LlamaIndex chat store backed by SuperLocalMemory V2.
131
+
132
+ Stores chat messages in SuperLocalMemory's local SQLite database,
133
+ keeping all data on-device with zero cloud calls.
134
+
135
+ Each message is stored as a separate SLM memory entry tagged with
136
+ ``li:chat:<hash>`` so different conversation sessions are cleanly
137
+ isolated. The full session key is preserved inside the serialized
138
+ content JSON for lossless round-tripping.
139
+
140
+ Args:
141
+ db_path: Optional path to the SQLite database file.
142
+ Defaults to ``~/.claude-memory/memory.db``.
143
+ """
144
+
145
+ # Pydantic fields ---------------------------------------------------
146
+ # We store the db_path as a string so the model stays JSON-serializable.
147
+ _db_path: Optional[str] = None
148
+ _store: Any = None # MemoryStoreV2 — not serializable, set in __init__
149
+
150
+ model_config = {"arbitrary_types_allowed": True}
151
+
152
+ def __init__(self, db_path: Optional[str] = None, **kwargs: Any) -> None:
153
+ """Initialize SuperLocalMemoryChatStore.
154
+
155
+ Args:
156
+ db_path: Optional path to the SLM SQLite database.
157
+ If None, uses the default ``~/.claude-memory/memory.db``.
158
+ """
159
+ super().__init__(**kwargs)
160
+ self._db_path = db_path
161
+ if db_path:
162
+ self._store = MemoryStoreV2(db_path=Path(db_path))
163
+ else:
164
+ self._store = MemoryStoreV2()
165
+
166
+ @classmethod
167
+ def class_name(cls) -> str:
168
+ """Get class name."""
169
+ return "SuperLocalMemoryChatStore"
170
+
171
+ # ------------------------------------------------------------------
172
+ # Internal helpers
173
+ # ------------------------------------------------------------------
174
+
175
+ def _get_memories_for_key(self, key: str) -> List[Dict[str, Any]]:
176
+ """Retrieve all SLM memories that belong to a chat session *key*.
177
+
178
+ Returns memories sorted by ``created_at`` ascending (oldest first)
179
+ to preserve conversation order.
180
+ """
181
+ tag = _make_tag(key)
182
+ all_memories = self._store.list_all(limit=_LIST_LIMIT)
183
+
184
+ matched: List[Dict[str, Any]] = []
185
+ for mem in all_memories:
186
+ mem_tags = mem.get("tags", [])
187
+ if isinstance(mem_tags, str):
188
+ try:
189
+ mem_tags = json.loads(mem_tags)
190
+ except (json.JSONDecodeError, TypeError):
191
+ mem_tags = [t.strip() for t in mem_tags.split(",") if t.strip()]
192
+ if tag in (mem_tags or []):
193
+ matched.append(mem)
194
+
195
+ # Sort by created_at ascending for correct conversation order
196
+ matched.sort(key=lambda m: m.get("created_at", ""))
197
+ return matched
198
+
199
+ def _memories_to_messages(
200
+ self, memories: List[Dict[str, Any]]
201
+ ) -> List[ChatMessage]:
202
+ """Convert a list of SLM memory dicts to ChatMessage objects."""
203
+ messages: List[ChatMessage] = []
204
+ for mem in memories:
205
+ msg = _deserialize_message(mem.get("content", ""))
206
+ if msg is not None:
207
+ messages.append(msg)
208
+ return messages
209
+
210
+ # ------------------------------------------------------------------
211
+ # BaseChatStore abstract method implementations
212
+ # ------------------------------------------------------------------
213
+
214
+ def set_messages(self, key: str, messages: List[ChatMessage]) -> None:
215
+ """Set messages for a key (replaces any existing messages)."""
216
+ self.delete_messages(key)
217
+ for message in messages:
218
+ self.add_message(key, message)
219
+
220
+ def get_messages(self, key: str) -> List[ChatMessage]:
221
+ """Get messages for a key, ordered by creation time."""
222
+ memories = self._get_memories_for_key(key)
223
+ return self._memories_to_messages(memories)
224
+
225
+ def add_message(self, key: str, message: ChatMessage) -> None:
226
+ """Add a single message for a key."""
227
+ content = _serialize_message(key, message)
228
+ tag = _make_tag(key)
229
+ self._store.add_memory(
230
+ content=content,
231
+ tags=[tag],
232
+ project_name=_PROJECT_NAME,
233
+ importance=_IMPORTANCE,
234
+ )
235
+
236
+ def delete_messages(self, key: str) -> Optional[List[ChatMessage]]:
237
+ """Delete all messages for a key.
238
+
239
+ Returns the deleted messages, or None if the key had no messages.
240
+ """
241
+ memories = self._get_memories_for_key(key)
242
+ if not memories:
243
+ return None
244
+
245
+ messages = self._memories_to_messages(memories)
246
+
247
+ for mem in memories:
248
+ self._store.delete_memory(mem["id"])
249
+
250
+ return messages
251
+
252
+ def delete_message(self, key: str, idx: int) -> Optional[ChatMessage]:
253
+ """Delete specific message for a key by index.
254
+
255
+ Args:
256
+ key: The session key.
257
+ idx: Zero-based index of the message to delete.
258
+
259
+ Returns:
260
+ The deleted ChatMessage, or None if index is out of range.
261
+ """
262
+ memories = self._get_memories_for_key(key)
263
+ if not memories or idx < 0 or idx >= len(memories):
264
+ return None
265
+
266
+ target = memories[idx]
267
+ msg = _deserialize_message(target.get("content", ""))
268
+ self._store.delete_memory(target["id"])
269
+ return msg
270
+
271
+ def delete_last_message(self, key: str) -> Optional[ChatMessage]:
272
+ """Delete the last (most recent) message for a key.
273
+
274
+ Returns:
275
+ The deleted ChatMessage, or None if the key has no messages.
276
+ """
277
+ memories = self._get_memories_for_key(key)
278
+ if not memories:
279
+ return None
280
+
281
+ last = memories[-1]
282
+ msg = _deserialize_message(last.get("content", ""))
283
+ self._store.delete_memory(last["id"])
284
+ return msg
285
+
286
+ def get_keys(self) -> List[str]:
287
+ """Get all unique session keys that have stored messages.
288
+
289
+ Keys are extracted from the serialized content JSON (the ``key``
290
+ field) rather than from tags, because tags contain only a hash of
291
+ the key for length-safety.
292
+ """
293
+ all_memories = self._store.list_all(limit=_LIST_LIMIT)
294
+ keys_seen: set[str] = set()
295
+
296
+ for mem in all_memories:
297
+ # Only consider memories whose tags indicate they belong to us
298
+ mem_tags = mem.get("tags", [])
299
+ if isinstance(mem_tags, str):
300
+ try:
301
+ mem_tags = json.loads(mem_tags)
302
+ except (json.JSONDecodeError, TypeError):
303
+ mem_tags = [t.strip() for t in mem_tags.split(",") if t.strip()]
304
+
305
+ is_ours = any(
306
+ isinstance(t, str) and t.startswith(_TAG_PREFIX)
307
+ for t in (mem_tags or [])
308
+ )
309
+ if not is_ours:
310
+ continue
311
+
312
+ key = _extract_key(mem.get("content", ""))
313
+ if key is not None:
314
+ keys_seen.add(key)
315
+
316
+ return sorted(keys_seen)
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "llama-index-storage-chat-store-superlocalmemory"
7
+ version = "0.1.0"
8
+ description = "LlamaIndex chat store backed by SuperLocalMemory — 100% local, zero cloud"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Varun Pratap Bhardwaj" },
14
+ ]
15
+ keywords = ["llamaindex", "llama-index", "memory", "local-first", "privacy", "sqlite", "chat-store", "mcp"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
22
+ ]
23
+ dependencies = [
24
+ "llama-index-core>=0.13.0,<0.15",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/varun369/SuperLocalMemoryV2"
29
+ Documentation = "https://superlocalmemory.com/"
30
+ Repository = "https://github.com/varun369/SuperLocalMemoryV2"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["llama_index"]
34
+
35
+ [tool.llamahub]
36
+ contains_example = false
37
+ import_path = "llama_index.storage.chat_store.superlocalmemory"
38
+
39
+ [tool.llamahub.class_authors]
40
+ SuperLocalMemoryChatStore = "varun369"
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
@@ -1,4 +1,3 @@
1
+ #!/usr/bin/env python3
1
2
  # SPDX-License-Identifier: MIT
2
3
  # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
- """Tests for lifecycle engine.
4
- """
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """SuperLocalMemory V2 - LlamaIndex Chat Store Tests
5
+
6
+ Comprehensive tests for the SuperLocalMemoryChatStore implementation.
7
+ All tests use temporary databases — no production data is touched.
8
+ """
9
+ import os
10
+ import sys
11
+ import tempfile
12
+
13
+ import pytest
14
+
15
+ # Ensure the SLM source tree is importable for tests
16
+ _SLM_SRC = os.path.join(
17
+ os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, "src"
18
+ )
19
+ _SLM_SRC = os.path.abspath(_SLM_SRC)
20
+ if _SLM_SRC not in sys.path:
21
+ sys.path.insert(0, _SLM_SRC)
22
+
23
+ from llama_index.core.base.llms.types import ChatMessage, MessageRole
24
+ from llama_index.storage.chat_store.superlocalmemory import SuperLocalMemoryChatStore
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Fixtures
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ @pytest.fixture
33
+ def tmp_db():
34
+ """Provide a path to a temporary database file that is cleaned up after use."""
35
+ with tempfile.TemporaryDirectory() as d:
36
+ yield os.path.join(d, "test_memory.db")
37
+
38
+
39
+ @pytest.fixture
40
+ def store(tmp_db):
41
+ """Provide a fresh SuperLocalMemoryChatStore backed by a temp database."""
42
+ return SuperLocalMemoryChatStore(db_path=tmp_db)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Helpers
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ def _make_messages(
51
+ texts: list[str], roles: list[MessageRole] | None = None
52
+ ) -> list[ChatMessage]:
53
+ """Create a list of ChatMessages from plain text strings."""
54
+ if roles is None:
55
+ # Alternate user / assistant
56
+ roles = [
57
+ MessageRole.USER if i % 2 == 0 else MessageRole.ASSISTANT
58
+ for i in range(len(texts))
59
+ ]
60
+ return [ChatMessage(role=r, content=t) for r, t in zip(roles, texts)]
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Tests
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ class TestSetAndGetMessages:
69
+ """Tests for set_messages / get_messages round-trip."""
70
+
71
+ def test_set_and_get_messages(self, store):
72
+ """Set 3 messages and retrieve them in order."""
73
+ msgs = _make_messages(["Hello", "Hi there", "How are you?"])
74
+ store.set_messages("session-1", msgs)
75
+
76
+ retrieved = store.get_messages("session-1")
77
+ assert len(retrieved) == 3
78
+ assert retrieved[0].content == "Hello"
79
+ assert retrieved[1].content == "Hi there"
80
+ assert retrieved[2].content == "How are you?"
81
+
82
+ def test_get_messages_empty(self, store):
83
+ """Getting messages for a non-existent key returns an empty list."""
84
+ assert store.get_messages("nonexistent") == []
85
+
86
+
87
+ class TestAddMessage:
88
+ """Tests for add_message."""
89
+
90
+ def test_add_single_message(self, store):
91
+ """Add one message and verify it appears in get_messages."""
92
+ msg = ChatMessage(role=MessageRole.USER, content="solo message")
93
+ store.add_message("key-a", msg)
94
+
95
+ retrieved = store.get_messages("key-a")
96
+ assert len(retrieved) == 1
97
+ assert retrieved[0].content == "solo message"
98
+ assert retrieved[0].role == MessageRole.USER
99
+
100
+
101
+ class TestDeleteMessages:
102
+ """Tests for delete_messages (all messages for a key)."""
103
+
104
+ def test_delete_messages(self, store):
105
+ """Set, delete, verify empty and check return value."""
106
+ msgs = _make_messages(["A", "B", "C"])
107
+ store.set_messages("del-key", msgs)
108
+
109
+ deleted = store.delete_messages("del-key")
110
+ assert deleted is not None
111
+ assert len(deleted) == 3
112
+ assert deleted[0].content == "A"
113
+
114
+ # After deletion, key should be empty
115
+ assert store.get_messages("del-key") == []
116
+
117
+ def test_delete_messages_nonexistent(self, store):
118
+ """Deleting messages for a key with no data returns None."""
119
+ assert store.delete_messages("ghost-key") is None
120
+
121
+
122
+ class TestDeleteMessageByIndex:
123
+ """Tests for delete_message (single message by index)."""
124
+
125
+ def test_delete_message_by_index(self, store):
126
+ """Set 3 messages, delete index 1, verify 2 remain in correct order."""
127
+ msgs = _make_messages(["first", "second", "third"])
128
+ store.set_messages("idx-key", msgs)
129
+
130
+ deleted = store.delete_message("idx-key", 1)
131
+ assert deleted is not None
132
+ assert deleted.content == "second"
133
+
134
+ remaining = store.get_messages("idx-key")
135
+ assert len(remaining) == 2
136
+ assert remaining[0].content == "first"
137
+ assert remaining[1].content == "third"
138
+
139
+ def test_delete_message_out_of_range(self, store):
140
+ """Deleting an out-of-range index returns None."""
141
+ msgs = _make_messages(["only"])
142
+ store.set_messages("range-key", msgs)
143
+ assert store.delete_message("range-key", 5) is None
144
+
145
+ def test_delete_message_negative_index(self, store):
146
+ """Deleting a negative index returns None."""
147
+ msgs = _make_messages(["x"])
148
+ store.set_messages("neg-key", msgs)
149
+ assert store.delete_message("neg-key", -1) is None
150
+
151
+
152
+ class TestDeleteLastMessage:
153
+ """Tests for delete_last_message."""
154
+
155
+ def test_delete_last_message(self, store):
156
+ """Set 3 messages, delete last, verify 2 remain."""
157
+ msgs = _make_messages(["alpha", "beta", "gamma"])
158
+ store.set_messages("last-key", msgs)
159
+
160
+ deleted = store.delete_last_message("last-key")
161
+ assert deleted is not None
162
+ assert deleted.content == "gamma"
163
+
164
+ remaining = store.get_messages("last-key")
165
+ assert len(remaining) == 2
166
+ assert remaining[-1].content == "beta"
167
+
168
+ def test_delete_last_message_empty(self, store):
169
+ """Deleting last message on an empty key returns None."""
170
+ assert store.delete_last_message("empty-key") is None
171
+
172
+
173
+ class TestGetKeys:
174
+ """Tests for get_keys."""
175
+
176
+ def test_get_keys(self, store):
177
+ """Set messages for 3 different keys and verify get_keys returns all 3."""
178
+ for key in ["alice", "bob", "carol"]:
179
+ store.add_message(key, ChatMessage(role=MessageRole.USER, content=f"hi from {key}"))
180
+
181
+ keys = store.get_keys()
182
+ assert sorted(keys) == ["alice", "bob", "carol"]
183
+
184
+ def test_get_keys_empty(self, store):
185
+ """No data returns empty list."""
186
+ assert store.get_keys() == []
187
+
188
+
189
+ class TestSessionIsolation:
190
+ """Tests that different keys don't interfere with each other."""
191
+
192
+ def test_session_isolation(self, store):
193
+ """Messages in key A should not appear in key B."""
194
+ store.set_messages("sess-A", _make_messages(["A1", "A2"]))
195
+ store.set_messages("sess-B", _make_messages(["B1", "B2", "B3"]))
196
+
197
+ a_msgs = store.get_messages("sess-A")
198
+ b_msgs = store.get_messages("sess-B")
199
+
200
+ assert len(a_msgs) == 2
201
+ assert len(b_msgs) == 3
202
+ assert a_msgs[0].content == "A1"
203
+ assert b_msgs[0].content == "B1"
204
+
205
+
206
+ class TestSetOverwrites:
207
+ """Tests that set_messages replaces previous messages."""
208
+
209
+ def test_set_overwrites(self, store):
210
+ """Setting messages twice replaces the first batch entirely."""
211
+ store.set_messages("ow-key", _make_messages(["old1", "old2", "old3"]))
212
+ store.set_messages("ow-key", _make_messages(["new1"]))
213
+
214
+ msgs = store.get_messages("ow-key")
215
+ assert len(msgs) == 1
216
+ assert msgs[0].content == "new1"
217
+
218
+
219
+ class TestPersistence:
220
+ """Tests that data survives across store instances."""
221
+
222
+ def test_persistence(self, tmp_db):
223
+ """Create store, add data, create NEW store on same db, verify data."""
224
+ store1 = SuperLocalMemoryChatStore(db_path=tmp_db)
225
+ store1.set_messages("persist-key", _make_messages(["remember me"]))
226
+
227
+ # New store instance pointing at same database
228
+ store2 = SuperLocalMemoryChatStore(db_path=tmp_db)
229
+ msgs = store2.get_messages("persist-key")
230
+ assert len(msgs) == 1
231
+ assert msgs[0].content == "remember me"
232
+
233
+
234
+ class TestContentEdgeCases:
235
+ """Tests for special content: unicode, long text, multiple roles."""
236
+
237
+ def test_unicode_content(self, store):
238
+ """Unicode characters (CJK, emoji, diacritics) round-trip correctly."""
239
+ texts = [
240
+ "Hello, world!",
241
+ "Hej varlden!",
242
+ "Bonjour le monde!",
243
+ ]
244
+ store.set_messages("unicode-key", _make_messages(texts))
245
+ msgs = store.get_messages("unicode-key")
246
+ assert len(msgs) == 3
247
+ assert msgs[1].content == "Hej varlden!"
248
+
249
+ def test_long_content(self, store):
250
+ """A 10K character message round-trips correctly."""
251
+ long_text = "x" * 10_000
252
+ store.add_message(
253
+ "long-key", ChatMessage(role=MessageRole.USER, content=long_text)
254
+ )
255
+ msgs = store.get_messages("long-key")
256
+ assert len(msgs) == 1
257
+ assert len(msgs[0].content) == 10_000
258
+
259
+ def test_message_roles(self, store):
260
+ """User, assistant, and system roles all round-trip correctly."""
261
+ msgs = [
262
+ ChatMessage(role=MessageRole.SYSTEM, content="You are helpful."),
263
+ ChatMessage(role=MessageRole.USER, content="Hi"),
264
+ ChatMessage(role=MessageRole.ASSISTANT, content="Hello!"),
265
+ ]
266
+ store.set_messages("roles-key", msgs)
267
+ retrieved = store.get_messages("roles-key")
268
+
269
+ assert retrieved[0].role == MessageRole.SYSTEM
270
+ assert retrieved[1].role == MessageRole.USER
271
+ assert retrieved[2].role == MessageRole.ASSISTANT
272
+
273
+ def test_empty_content_message(self, store):
274
+ """A message with empty content still round-trips."""
275
+ store.add_message(
276
+ "empty-msg", ChatMessage(role=MessageRole.ASSISTANT, content="")
277
+ )
278
+ msgs = store.get_messages("empty-msg")
279
+ assert len(msgs) == 1
280
+ # Empty content serializes as "" and deserializes back
281
+ assert msgs[0].content == "" or msgs[0].content is None
282
+
283
+ def test_additional_kwargs(self, store):
284
+ """additional_kwargs round-trip correctly."""
285
+ msg = ChatMessage(
286
+ role=MessageRole.USER,
287
+ content="with metadata",
288
+ additional_kwargs={"source": "test", "count": 42},
289
+ )
290
+ store.add_message("kwargs-key", msg)
291
+ msgs = store.get_messages("kwargs-key")
292
+ assert len(msgs) == 1
293
+ assert msgs[0].additional_kwargs.get("source") == "test"
294
+ assert msgs[0].additional_kwargs.get("count") == 42