superlocalmemory 4.1.13 → 4.1.14

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 (740) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +3 -3
  4. package/package.json +1 -8
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/CLAUDE.md +3 -3
  7. package/plugin/agents/slm-governance-advisor.md +1 -1
  8. package/plugin/agents/slm-loop-runner.md +1 -1
  9. package/plugin/agents/slm-memory-advisor.md +1 -1
  10. package/plugin/agents/slm-optimize-advisor.md +1 -1
  11. package/plugin/requirements.txt +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +1 -1
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +1 -1
  19. package/plugin/skills/slm-recall/SKILL.md +1 -1
  20. package/plugin/skills/slm-remember/SKILL.md +1 -1
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +1 -1
  23. package/plugin/skills/slm-status/SKILL.md +1 -1
  24. package/plugin-src/agents/slm-memory-advisor.md +1 -1
  25. package/plugin-src/agents/slm-optimize-advisor.md +1 -1
  26. package/plugin-src/rules/AGENTS.md +1 -1
  27. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  33. package/plugin-src/skills/slm-profile/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  36. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  38. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  39. package/scripts/postinstall.js +71 -2
  40. package/pyproject.toml +0 -250
  41. package/src/superlocalmemory/__init__.py +0 -82
  42. package/src/superlocalmemory/access/__init__.py +0 -3
  43. package/src/superlocalmemory/access/rbac.py +0 -575
  44. package/src/superlocalmemory/attribution/__init__.py +0 -9
  45. package/src/superlocalmemory/attribution/signer.py +0 -173
  46. package/src/superlocalmemory/attribution/watermark.py +0 -189
  47. package/src/superlocalmemory/brain/__init__.py +0 -5
  48. package/src/superlocalmemory/brain/truth.py +0 -418
  49. package/src/superlocalmemory/cli/__init__.py +0 -5
  50. package/src/superlocalmemory/cli/__main__.py +0 -17
  51. package/src/superlocalmemory/cli/_lazy_init.py +0 -115
  52. package/src/superlocalmemory/cli/cache_cmd.py +0 -198
  53. package/src/superlocalmemory/cli/commands.py +0 -4710
  54. package/src/superlocalmemory/cli/compress_cmd.py +0 -151
  55. package/src/superlocalmemory/cli/context_commands.py +0 -193
  56. package/src/superlocalmemory/cli/daemon.py +0 -909
  57. package/src/superlocalmemory/cli/db_migrate.py +0 -150
  58. package/src/superlocalmemory/cli/diagnostics_cmd.py +0 -101
  59. package/src/superlocalmemory/cli/escape_hatch.py +0 -220
  60. package/src/superlocalmemory/cli/evidence_cmd.py +0 -103
  61. package/src/superlocalmemory/cli/gdpr_cmd.py +0 -792
  62. package/src/superlocalmemory/cli/gdpr_io.py +0 -109
  63. package/src/superlocalmemory/cli/help_cmd.py +0 -197
  64. package/src/superlocalmemory/cli/host_upgrades.py +0 -189
  65. package/src/superlocalmemory/cli/ingest_cmd.py +0 -327
  66. package/src/superlocalmemory/cli/json_output.py +0 -81
  67. package/src/superlocalmemory/cli/loop_cmd.py +0 -187
  68. package/src/superlocalmemory/cli/main.py +0 -1145
  69. package/src/superlocalmemory/cli/mesh_cmd.py +0 -38
  70. package/src/superlocalmemory/cli/migrate_cmd.py +0 -55
  71. package/src/superlocalmemory/cli/ops_cmd.py +0 -281
  72. package/src/superlocalmemory/cli/optimize_cmd.py +0 -179
  73. package/src/superlocalmemory/cli/optimize_constants.py +0 -31
  74. package/src/superlocalmemory/cli/pending_store.py +0 -296
  75. package/src/superlocalmemory/cli/proxy_cmd.py +0 -108
  76. package/src/superlocalmemory/cli/scale_engine_cmd.py +0 -56
  77. package/src/superlocalmemory/cli/service_installer.py +0 -373
  78. package/src/superlocalmemory/cli/setup_wizard.py +0 -1162
  79. package/src/superlocalmemory/cli/summary_cmd.py +0 -215
  80. package/src/superlocalmemory/cli/version_banner.py +0 -202
  81. package/src/superlocalmemory/cli/wizard_v3426_options.py +0 -129
  82. package/src/superlocalmemory/code_graph/__init__.py +0 -46
  83. package/src/superlocalmemory/code_graph/blast_radius.py +0 -177
  84. package/src/superlocalmemory/code_graph/bridge/__init__.py +0 -36
  85. package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +0 -490
  86. package/src/superlocalmemory/code_graph/bridge/event_listeners.py +0 -206
  87. package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +0 -159
  88. package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +0 -170
  89. package/src/superlocalmemory/code_graph/bridge/maintenance.py +0 -220
  90. package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +0 -152
  91. package/src/superlocalmemory/code_graph/changes.py +0 -363
  92. package/src/superlocalmemory/code_graph/communities.py +0 -299
  93. package/src/superlocalmemory/code_graph/config.py +0 -154
  94. package/src/superlocalmemory/code_graph/database.py +0 -526
  95. package/src/superlocalmemory/code_graph/extractors/__init__.py +0 -95
  96. package/src/superlocalmemory/code_graph/extractors/python.py +0 -413
  97. package/src/superlocalmemory/code_graph/extractors/typescript.py +0 -556
  98. package/src/superlocalmemory/code_graph/flows.py +0 -350
  99. package/src/superlocalmemory/code_graph/git_hooks.py +0 -226
  100. package/src/superlocalmemory/code_graph/graph_engine.py +0 -295
  101. package/src/superlocalmemory/code_graph/graph_store.py +0 -335
  102. package/src/superlocalmemory/code_graph/incremental.py +0 -200
  103. package/src/superlocalmemory/code_graph/models.py +0 -130
  104. package/src/superlocalmemory/code_graph/parser.py +0 -687
  105. package/src/superlocalmemory/code_graph/resolver.py +0 -321
  106. package/src/superlocalmemory/code_graph/search.py +0 -460
  107. package/src/superlocalmemory/code_graph/service.py +0 -95
  108. package/src/superlocalmemory/code_graph/watcher.py +0 -207
  109. package/src/superlocalmemory/compliance/__init__.py +0 -0
  110. package/src/superlocalmemory/compliance/abac.py +0 -204
  111. package/src/superlocalmemory/compliance/audit.py +0 -385
  112. package/src/superlocalmemory/compliance/eu_ai_act.py +0 -101
  113. package/src/superlocalmemory/compliance/gdpr.py +0 -1479
  114. package/src/superlocalmemory/compliance/lifecycle.py +0 -158
  115. package/src/superlocalmemory/compliance/retention.py +0 -415
  116. package/src/superlocalmemory/compliance/scheduler.py +0 -217
  117. package/src/superlocalmemory/contracts/__init__.py +0 -1
  118. package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +0 -92
  119. package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +0 -46
  120. package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +0 -59
  121. package/src/superlocalmemory/contracts/v402.py +0 -62
  122. package/src/superlocalmemory/core/__init__.py +0 -0
  123. package/src/superlocalmemory/core/actor_context.py +0 -166
  124. package/src/superlocalmemory/core/admission.py +0 -769
  125. package/src/superlocalmemory/core/backend_orchestrator.py +0 -637
  126. package/src/superlocalmemory/core/block_hygiene.py +0 -147
  127. package/src/superlocalmemory/core/community_summary.py +0 -267
  128. package/src/superlocalmemory/core/component_healer.py +0 -144
  129. package/src/superlocalmemory/core/component_registry.py +0 -514
  130. package/src/superlocalmemory/core/config.py +0 -2204
  131. package/src/superlocalmemory/core/consolidation_engine.py +0 -983
  132. package/src/superlocalmemory/core/context_cache.py +0 -574
  133. package/src/superlocalmemory/core/derivation_lineage.py +0 -246
  134. package/src/superlocalmemory/core/embedding_worker.py +0 -208
  135. package/src/superlocalmemory/core/embeddings.py +0 -1052
  136. package/src/superlocalmemory/core/engine.py +0 -1395
  137. package/src/superlocalmemory/core/engine_capabilities.py +0 -24
  138. package/src/superlocalmemory/core/engine_ingestion.py +0 -983
  139. package/src/superlocalmemory/core/engine_lock.py +0 -75
  140. package/src/superlocalmemory/core/engine_wiring.py +0 -776
  141. package/src/superlocalmemory/core/entity_community.py +0 -178
  142. package/src/superlocalmemory/core/error_envelope.py +0 -60
  143. package/src/superlocalmemory/core/evidence_bundle.py +0 -528
  144. package/src/superlocalmemory/core/fact_consolidator.py +0 -812
  145. package/src/superlocalmemory/core/file_lock.py +0 -92
  146. package/src/superlocalmemory/core/graph_analyzer.py +0 -456
  147. package/src/superlocalmemory/core/graph_metrics.py +0 -597
  148. package/src/superlocalmemory/core/graph_pruner.py +0 -939
  149. package/src/superlocalmemory/core/health_monitor.py +0 -338
  150. package/src/superlocalmemory/core/hooks.py +0 -65
  151. package/src/superlocalmemory/core/ingest_gate.py +0 -133
  152. package/src/superlocalmemory/core/ingest_policy.py +0 -38
  153. package/src/superlocalmemory/core/ingestion_command.py +0 -1042
  154. package/src/superlocalmemory/core/injection.py +0 -434
  155. package/src/superlocalmemory/core/install_detector.py +0 -131
  156. package/src/superlocalmemory/core/key_expander.py +0 -138
  157. package/src/superlocalmemory/core/lifecycle_state.py +0 -153
  158. package/src/superlocalmemory/core/maintenance.py +0 -777
  159. package/src/superlocalmemory/core/maintenance_scheduler.py +0 -507
  160. package/src/superlocalmemory/core/materialization_control.py +0 -20
  161. package/src/superlocalmemory/core/mcp_embedder_proxy.py +0 -89
  162. package/src/superlocalmemory/core/memory_health.py +0 -266
  163. package/src/superlocalmemory/core/mode_capability.py +0 -111
  164. package/src/superlocalmemory/core/modes.py +0 -168
  165. package/src/superlocalmemory/core/mutations.py +0 -688
  166. package/src/superlocalmemory/core/ollama_embedder.py +0 -266
  167. package/src/superlocalmemory/core/ollama_validator.py +0 -315
  168. package/src/superlocalmemory/core/operation_policy.py +0 -92
  169. package/src/superlocalmemory/core/operation_policy_registry.py +0 -542
  170. package/src/superlocalmemory/core/operation_request.py +0 -127
  171. package/src/superlocalmemory/core/ops_remediation.py +0 -542
  172. package/src/superlocalmemory/core/pii.py +0 -105
  173. package/src/superlocalmemory/core/platform_utils.py +0 -138
  174. package/src/superlocalmemory/core/profiles.py +0 -234
  175. package/src/superlocalmemory/core/progressive_abstraction.py +0 -208
  176. package/src/superlocalmemory/core/projection_drain.py +0 -380
  177. package/src/superlocalmemory/core/queue_consumer.py +0 -168
  178. package/src/superlocalmemory/core/ram_lock.py +0 -160
  179. package/src/superlocalmemory/core/rate_limit.py +0 -151
  180. package/src/superlocalmemory/core/recall_gate.py +0 -95
  181. package/src/superlocalmemory/core/recall_pipeline.py +0 -1337
  182. package/src/superlocalmemory/core/recall_queue.py +0 -377
  183. package/src/superlocalmemory/core/recall_worker.py +0 -414
  184. package/src/superlocalmemory/core/registry.py +0 -121
  185. package/src/superlocalmemory/core/remember_admission.py +0 -161
  186. package/src/superlocalmemory/core/remember_runtime.py +0 -1190
  187. package/src/superlocalmemory/core/remote_mode.py +0 -214
  188. package/src/superlocalmemory/core/reranker_worker.py +0 -338
  189. package/src/superlocalmemory/core/safe_fs.py +0 -108
  190. package/src/superlocalmemory/core/scale_autopromote.py +0 -196
  191. package/src/superlocalmemory/core/scale_engine.py +0 -915
  192. package/src/superlocalmemory/core/score_contract.py +0 -82
  193. package/src/superlocalmemory/core/security_primitives.py +0 -672
  194. package/src/superlocalmemory/core/session_identity.py +0 -98
  195. package/src/superlocalmemory/core/shadow_router.py +0 -319
  196. package/src/superlocalmemory/core/slm_disabled.py +0 -85
  197. package/src/superlocalmemory/core/status_contract.py +0 -108
  198. package/src/superlocalmemory/core/store_pipeline.py +0 -1404
  199. package/src/superlocalmemory/core/summarizer.py +0 -200
  200. package/src/superlocalmemory/core/tier_manager.py +0 -461
  201. package/src/superlocalmemory/core/topic_signature.py +0 -156
  202. package/src/superlocalmemory/core/transactions/__init__.py +0 -78
  203. package/src/superlocalmemory/core/transactions/concrete_owners.py +0 -604
  204. package/src/superlocalmemory/core/transactions/erasure.py +0 -825
  205. package/src/superlocalmemory/core/transactions/manifest.py +0 -255
  206. package/src/superlocalmemory/core/transactions/manifest_key.py +0 -155
  207. package/src/superlocalmemory/core/transactions/obligations.py +0 -272
  208. package/src/superlocalmemory/core/transactions/owners.py +0 -114
  209. package/src/superlocalmemory/core/transactions/reconciler.py +0 -285
  210. package/src/superlocalmemory/core/transactions/service.py +0 -330
  211. package/src/superlocalmemory/core/worker_pool.py +0 -377
  212. package/src/superlocalmemory/core/working_memory.py +0 -288
  213. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  214. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +0 -374
  215. package/src/superlocalmemory/dynamics/eap_scheduler.py +0 -294
  216. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +0 -171
  217. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +0 -227
  218. package/src/superlocalmemory/encoding/__init__.py +0 -0
  219. package/src/superlocalmemory/encoding/auto_linker.py +0 -308
  220. package/src/superlocalmemory/encoding/cognitive_consolidator.py +0 -899
  221. package/src/superlocalmemory/encoding/consolidator.py +0 -472
  222. package/src/superlocalmemory/encoding/context_generator.py +0 -175
  223. package/src/superlocalmemory/encoding/emotional.py +0 -189
  224. package/src/superlocalmemory/encoding/entity_reflexion.py +0 -200
  225. package/src/superlocalmemory/encoding/entity_resolver.py +0 -687
  226. package/src/superlocalmemory/encoding/entropy_gate.py +0 -101
  227. package/src/superlocalmemory/encoding/fact_extractor.py +0 -877
  228. package/src/superlocalmemory/encoding/foresight.py +0 -93
  229. package/src/superlocalmemory/encoding/graph_builder.py +0 -346
  230. package/src/superlocalmemory/encoding/observation_builder.py +0 -177
  231. package/src/superlocalmemory/encoding/prospective_markers.py +0 -262
  232. package/src/superlocalmemory/encoding/scene_builder.py +0 -410
  233. package/src/superlocalmemory/encoding/signal_inference.py +0 -90
  234. package/src/superlocalmemory/encoding/temporal_parser.py +0 -432
  235. package/src/superlocalmemory/encoding/temporal_validator.py +0 -572
  236. package/src/superlocalmemory/encoding/type_router.py +0 -237
  237. package/src/superlocalmemory/evaluation/__init__.py +0 -13
  238. package/src/superlocalmemory/evaluation/calibration.py +0 -308
  239. package/src/superlocalmemory/evolution/__init__.py +0 -29
  240. package/src/superlocalmemory/evolution/blind_verifier.py +0 -122
  241. package/src/superlocalmemory/evolution/budget.py +0 -356
  242. package/src/superlocalmemory/evolution/evolution_store.py +0 -619
  243. package/src/superlocalmemory/evolution/llm_dispatch.py +0 -559
  244. package/src/superlocalmemory/evolution/model_selection.py +0 -175
  245. package/src/superlocalmemory/evolution/mutation_generator.py +0 -226
  246. package/src/superlocalmemory/evolution/skill_activator.py +0 -270
  247. package/src/superlocalmemory/evolution/skill_evolver.py +0 -928
  248. package/src/superlocalmemory/evolution/triggers.py +0 -376
  249. package/src/superlocalmemory/evolution/types.py +0 -114
  250. package/src/superlocalmemory/graph/__init__.py +0 -9
  251. package/src/superlocalmemory/graph/cozo_adjacency.py +0 -122
  252. package/src/superlocalmemory/graph/cozo_backend.py +0 -751
  253. package/src/superlocalmemory/hooks/__init__.py +0 -3
  254. package/src/superlocalmemory/hooks/_outcome_common.py +0 -523
  255. package/src/superlocalmemory/hooks/adapter_base.py +0 -347
  256. package/src/superlocalmemory/hooks/antigravity_adapter.py +0 -171
  257. package/src/superlocalmemory/hooks/auto_capture.py +0 -133
  258. package/src/superlocalmemory/hooks/auto_invoker.py +0 -521
  259. package/src/superlocalmemory/hooks/auto_parameterize.py +0 -147
  260. package/src/superlocalmemory/hooks/auto_recall.py +0 -191
  261. package/src/superlocalmemory/hooks/auto_recall_hook.py +0 -251
  262. package/src/superlocalmemory/hooks/before_web_hook.py +0 -131
  263. package/src/superlocalmemory/hooks/claude_code_hooks.py +0 -637
  264. package/src/superlocalmemory/hooks/codex_assets.py +0 -251
  265. package/src/superlocalmemory/hooks/codex_hooks.py +0 -186
  266. package/src/superlocalmemory/hooks/context_payload.py +0 -311
  267. package/src/superlocalmemory/hooks/copilot_adapter.py +0 -208
  268. package/src/superlocalmemory/hooks/cross_platform_connector.py +0 -90
  269. package/src/superlocalmemory/hooks/cursor_adapter.py +0 -173
  270. package/src/superlocalmemory/hooks/hook_daemon.py +0 -295
  271. package/src/superlocalmemory/hooks/hook_handlers.py +0 -822
  272. package/src/superlocalmemory/hooks/ide_connector.py +0 -246
  273. package/src/superlocalmemory/hooks/memory_protocol.py +0 -158
  274. package/src/superlocalmemory/hooks/portable_kit.py +0 -755
  275. package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -183
  276. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +0 -351
  277. package/src/superlocalmemory/hooks/prewarm_auth.py +0 -187
  278. package/src/superlocalmemory/hooks/rules_engine.py +0 -99
  279. package/src/superlocalmemory/hooks/session_registry.py +0 -330
  280. package/src/superlocalmemory/hooks/stop_outcome_hook.py +0 -138
  281. package/src/superlocalmemory/hooks/sync_loop.py +0 -114
  282. package/src/superlocalmemory/hooks/topic_shift_hook.py +0 -302
  283. package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -131
  284. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +0 -210
  285. package/src/superlocalmemory/infra/__init__.py +0 -3
  286. package/src/superlocalmemory/infra/auth_middleware.py +0 -145
  287. package/src/superlocalmemory/infra/backup.py +0 -974
  288. package/src/superlocalmemory/infra/backup_obligations.py +0 -423
  289. package/src/superlocalmemory/infra/cache_manager.py +0 -267
  290. package/src/superlocalmemory/infra/cloud_backup.py +0 -788
  291. package/src/superlocalmemory/infra/daemon_identity.py +0 -300
  292. package/src/superlocalmemory/infra/data_root.py +0 -238
  293. package/src/superlocalmemory/infra/event_bus.py +0 -637
  294. package/src/superlocalmemory/infra/local_diagnostics.py +0 -327
  295. package/src/superlocalmemory/infra/pid_manager.py +0 -193
  296. package/src/superlocalmemory/infra/process_identity.py +0 -180
  297. package/src/superlocalmemory/infra/process_reaper.py +0 -624
  298. package/src/superlocalmemory/infra/rate_limiter.py +0 -228
  299. package/src/superlocalmemory/infra/self_heal.py +0 -401
  300. package/src/superlocalmemory/infra/version_integrity.py +0 -229
  301. package/src/superlocalmemory/ingestion/__init__.py +0 -13
  302. package/src/superlocalmemory/ingestion/adapter_manager.py +0 -255
  303. package/src/superlocalmemory/ingestion/base_adapter.py +0 -171
  304. package/src/superlocalmemory/ingestion/calendar_adapter.py +0 -349
  305. package/src/superlocalmemory/ingestion/credentials.py +0 -125
  306. package/src/superlocalmemory/ingestion/gmail_adapter.py +0 -378
  307. package/src/superlocalmemory/ingestion/parsers.py +0 -100
  308. package/src/superlocalmemory/ingestion/transcript_adapter.py +0 -161
  309. package/src/superlocalmemory/integrations/__init__.py +0 -1
  310. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +0 -431
  311. package/src/superlocalmemory/integrations/bounded_loops_v051.py +0 -236
  312. package/src/superlocalmemory/learning/__init__.py +0 -0
  313. package/src/superlocalmemory/learning/adaptive.py +0 -172
  314. package/src/superlocalmemory/learning/arm_catalog.py +0 -97
  315. package/src/superlocalmemory/learning/assertion_miner.py +0 -403
  316. package/src/superlocalmemory/learning/bandit.py +0 -654
  317. package/src/superlocalmemory/learning/bandit_cache.py +0 -131
  318. package/src/superlocalmemory/learning/behavioral.py +0 -542
  319. package/src/superlocalmemory/learning/bootstrap.py +0 -298
  320. package/src/superlocalmemory/learning/consolidation_cycle.py +0 -398
  321. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +0 -115
  322. package/src/superlocalmemory/learning/consolidation_worker.py +0 -261
  323. package/src/superlocalmemory/learning/cross_project.py +0 -408
  324. package/src/superlocalmemory/learning/database.py +0 -698
  325. package/src/superlocalmemory/learning/dedup_hnsw.py +0 -413
  326. package/src/superlocalmemory/learning/engagement.py +0 -487
  327. package/src/superlocalmemory/learning/engagement_features.py +0 -279
  328. package/src/superlocalmemory/learning/ensemble.py +0 -309
  329. package/src/superlocalmemory/learning/entity_compiler.py +0 -356
  330. package/src/superlocalmemory/learning/fact_outcome_joins.py +0 -207
  331. package/src/superlocalmemory/learning/features.py +0 -138
  332. package/src/superlocalmemory/learning/feedback.py +0 -724
  333. package/src/superlocalmemory/learning/forgetting_scheduler.py +0 -375
  334. package/src/superlocalmemory/learning/hnsw_dedup.py +0 -69
  335. package/src/superlocalmemory/learning/labeler.py +0 -85
  336. package/src/superlocalmemory/learning/legacy_migration.py +0 -316
  337. package/src/superlocalmemory/learning/lightgbm_subprocess.py +0 -236
  338. package/src/superlocalmemory/learning/memory_merge.py +0 -175
  339. package/src/superlocalmemory/learning/model_cache.py +0 -267
  340. package/src/superlocalmemory/learning/model_rollback.py +0 -281
  341. package/src/superlocalmemory/learning/outcome_queue.py +0 -306
  342. package/src/superlocalmemory/learning/outcomes.py +0 -286
  343. package/src/superlocalmemory/learning/pattern_miner.py +0 -465
  344. package/src/superlocalmemory/learning/pattern_miner_constants.py +0 -90
  345. package/src/superlocalmemory/learning/pcos.py +0 -291
  346. package/src/superlocalmemory/learning/project_context.py +0 -366
  347. package/src/superlocalmemory/learning/propensity.py +0 -131
  348. package/src/superlocalmemory/learning/ranker.py +0 -300
  349. package/src/superlocalmemory/learning/ranker_common.py +0 -163
  350. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +0 -210
  351. package/src/superlocalmemory/learning/ranker_retrain_online.py +0 -423
  352. package/src/superlocalmemory/learning/reward.py +0 -888
  353. package/src/superlocalmemory/learning/reward_archive.py +0 -223
  354. package/src/superlocalmemory/learning/reward_boost.py +0 -211
  355. package/src/superlocalmemory/learning/reward_from_outcomes.py +0 -365
  356. package/src/superlocalmemory/learning/reward_model.py +0 -144
  357. package/src/superlocalmemory/learning/reward_proxy.py +0 -578
  358. package/src/superlocalmemory/learning/shadow_test.py +0 -524
  359. package/src/superlocalmemory/learning/signal_kinds.py +0 -79
  360. package/src/superlocalmemory/learning/signal_worker.py +0 -268
  361. package/src/superlocalmemory/learning/signals.py +0 -646
  362. package/src/superlocalmemory/learning/skill_performance_miner.py +0 -422
  363. package/src/superlocalmemory/learning/source_quality.py +0 -828
  364. package/src/superlocalmemory/learning/trigram_index.py +0 -548
  365. package/src/superlocalmemory/learning/workflows.py +0 -309
  366. package/src/superlocalmemory/llm/__init__.py +0 -0
  367. package/src/superlocalmemory/llm/backbone.py +0 -364
  368. package/src/superlocalmemory/loops/__init__.py +0 -58
  369. package/src/superlocalmemory/loops/budget.py +0 -58
  370. package/src/superlocalmemory/loops/engine.py +0 -174
  371. package/src/superlocalmemory/loops/ledger.py +0 -298
  372. package/src/superlocalmemory/loops/models.py +0 -152
  373. package/src/superlocalmemory/loops/rules.py +0 -52
  374. package/src/superlocalmemory/math/__init__.py +0 -0
  375. package/src/superlocalmemory/math/ebbinghaus.py +0 -352
  376. package/src/superlocalmemory/math/fisher.py +0 -356
  377. package/src/superlocalmemory/math/fisher_quantized.py +0 -255
  378. package/src/superlocalmemory/math/hopfield.py +0 -282
  379. package/src/superlocalmemory/math/langevin.py +0 -411
  380. package/src/superlocalmemory/math/polar_quant.py +0 -414
  381. package/src/superlocalmemory/math/qjl.py +0 -115
  382. package/src/superlocalmemory/math/sheaf.py +0 -261
  383. package/src/superlocalmemory/math/turbo_quant.py +0 -318
  384. package/src/superlocalmemory/mcp/__init__.py +0 -0
  385. package/src/superlocalmemory/mcp/_daemon_proxy.py +0 -203
  386. package/src/superlocalmemory/mcp/_pool_adapter.py +0 -181
  387. package/src/superlocalmemory/mcp/_stdin_guard.py +0 -60
  388. package/src/superlocalmemory/mcp/agent_context.py +0 -115
  389. package/src/superlocalmemory/mcp/cli_fallback.py +0 -602
  390. package/src/superlocalmemory/mcp/http_transport.py +0 -85
  391. package/src/superlocalmemory/mcp/profiles.py +0 -154
  392. package/src/superlocalmemory/mcp/resources.py +0 -281
  393. package/src/superlocalmemory/mcp/server.py +0 -480
  394. package/src/superlocalmemory/mcp/session_binding.py +0 -98
  395. package/src/superlocalmemory/mcp/shared.py +0 -112
  396. package/src/superlocalmemory/mcp/tools.py +0 -18
  397. package/src/superlocalmemory/mcp/tools_active.py +0 -958
  398. package/src/superlocalmemory/mcp/tools_brain.py +0 -298
  399. package/src/superlocalmemory/mcp/tools_code_graph.py +0 -1717
  400. package/src/superlocalmemory/mcp/tools_context.py +0 -239
  401. package/src/superlocalmemory/mcp/tools_core.py +0 -1112
  402. package/src/superlocalmemory/mcp/tools_evolution.py +0 -343
  403. package/src/superlocalmemory/mcp/tools_learning.py +0 -393
  404. package/src/superlocalmemory/mcp/tools_loops.py +0 -345
  405. package/src/superlocalmemory/mcp/tools_mesh.py +0 -429
  406. package/src/superlocalmemory/mcp/tools_ops.py +0 -115
  407. package/src/superlocalmemory/mcp/tools_optimize.py +0 -322
  408. package/src/superlocalmemory/mcp/tools_summaries.py +0 -147
  409. package/src/superlocalmemory/mcp/tools_v28.py +0 -292
  410. package/src/superlocalmemory/mcp/tools_v3.py +0 -398
  411. package/src/superlocalmemory/mcp/tools_v33.py +0 -507
  412. package/src/superlocalmemory/mesh/__init__.py +0 -12
  413. package/src/superlocalmemory/mesh/broker.py +0 -812
  414. package/src/superlocalmemory/mesh/broker_security.py +0 -470
  415. package/src/superlocalmemory/mesh/discovery.py +0 -365
  416. package/src/superlocalmemory/mesh/lock_protocol.py +0 -313
  417. package/src/superlocalmemory/mesh/node_identity.py +0 -97
  418. package/src/superlocalmemory/mesh/outbox_remote.py +0 -429
  419. package/src/superlocalmemory/mesh/remote_sync.py +0 -829
  420. package/src/superlocalmemory/mesh/state_sync.py +0 -286
  421. package/src/superlocalmemory/migrations/__init__.py +0 -5
  422. package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +0 -144
  423. package/src/superlocalmemory/optimize/NOTICE +0 -6
  424. package/src/superlocalmemory/optimize/__init__.py +0 -0
  425. package/src/superlocalmemory/optimize/adapters/__init__.py +0 -68
  426. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +0 -120
  427. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +0 -112
  428. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +0 -122
  429. package/src/superlocalmemory/optimize/adapters/wrap.py +0 -228
  430. package/src/superlocalmemory/optimize/cache/__init__.py +0 -31
  431. package/src/superlocalmemory/optimize/cache/boundary_store.py +0 -488
  432. package/src/superlocalmemory/optimize/cache/centroid_store.py +0 -199
  433. package/src/superlocalmemory/optimize/cache/context_key.py +0 -67
  434. package/src/superlocalmemory/optimize/cache/exact.py +0 -88
  435. package/src/superlocalmemory/optimize/cache/invalidation.py +0 -36
  436. package/src/superlocalmemory/optimize/cache/key_builder.py +0 -111
  437. package/src/superlocalmemory/optimize/cache/manager.py +0 -737
  438. package/src/superlocalmemory/optimize/cache/semantic.py +0 -637
  439. package/src/superlocalmemory/optimize/cache/stampede.py +0 -50
  440. package/src/superlocalmemory/optimize/compress/__init__.py +0 -17
  441. package/src/superlocalmemory/optimize/compress/align.py +0 -159
  442. package/src/superlocalmemory/optimize/compress/ccr.py +0 -116
  443. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +0 -71
  444. package/src/superlocalmemory/optimize/compress/router.py +0 -667
  445. package/src/superlocalmemory/optimize/config/__init__.py +0 -56
  446. package/src/superlocalmemory/optimize/config/defaults.py +0 -43
  447. package/src/superlocalmemory/optimize/config/schema.py +0 -327
  448. package/src/superlocalmemory/optimize/config/store.py +0 -270
  449. package/src/superlocalmemory/optimize/metrics/__init__.py +0 -8
  450. package/src/superlocalmemory/optimize/metrics/counters.py +0 -155
  451. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -87
  452. package/src/superlocalmemory/optimize/metrics/exporters.py +0 -77
  453. package/src/superlocalmemory/optimize/metrics/persistence.py +0 -115
  454. package/src/superlocalmemory/optimize/proxy/__init__.py +0 -28
  455. package/src/superlocalmemory/optimize/proxy/_helpers.py +0 -730
  456. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +0 -375
  457. package/src/superlocalmemory/optimize/proxy/capture.py +0 -550
  458. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +0 -528
  459. package/src/superlocalmemory/optimize/proxy/lifecycle.py +0 -126
  460. package/src/superlocalmemory/optimize/proxy/openai_surface.py +0 -465
  461. package/src/superlocalmemory/optimize/proxy/server.py +0 -199
  462. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +0 -246
  463. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  464. package/src/superlocalmemory/optimize/storage/db.py +0 -1185
  465. package/src/superlocalmemory/optimize/storage/schema.py +0 -205
  466. package/src/superlocalmemory/parameterization/__init__.py +0 -47
  467. package/src/superlocalmemory/parameterization/cross_project.py +0 -12
  468. package/src/superlocalmemory/parameterization/pattern_extractor.py +0 -584
  469. package/src/superlocalmemory/parameterization/pii_filter.py +0 -106
  470. package/src/superlocalmemory/parameterization/prompt_injector.py +0 -219
  471. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +0 -281
  472. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +0 -542
  473. package/src/superlocalmemory/parameterization/workflow_miner.py +0 -17
  474. package/src/superlocalmemory/reliability/__init__.py +0 -45
  475. package/src/superlocalmemory/reliability/join_liveness.py +0 -301
  476. package/src/superlocalmemory/reliability/prior_distance.py +0 -243
  477. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  478. package/src/superlocalmemory/retrieval/agentic.py +0 -367
  479. package/src/superlocalmemory/retrieval/ann_index.py +0 -235
  480. package/src/superlocalmemory/retrieval/bm25_channel.py +0 -451
  481. package/src/superlocalmemory/retrieval/bridge_discovery.py +0 -253
  482. package/src/superlocalmemory/retrieval/channel_registry.py +0 -154
  483. package/src/superlocalmemory/retrieval/channel_status.py +0 -117
  484. package/src/superlocalmemory/retrieval/engine.py +0 -1615
  485. package/src/superlocalmemory/retrieval/entity_channel.py +0 -994
  486. package/src/superlocalmemory/retrieval/forgetting_filter.py +0 -160
  487. package/src/superlocalmemory/retrieval/fusion.py +0 -81
  488. package/src/superlocalmemory/retrieval/graph_adjacency.py +0 -219
  489. package/src/superlocalmemory/retrieval/hopfield_channel.py +0 -465
  490. package/src/superlocalmemory/retrieval/profile_channel.py +0 -105
  491. package/src/superlocalmemory/retrieval/quantization_aware_search.py +0 -147
  492. package/src/superlocalmemory/retrieval/remote_reranker.py +0 -758
  493. package/src/superlocalmemory/retrieval/reranker.py +0 -674
  494. package/src/superlocalmemory/retrieval/scope_policy.py +0 -126
  495. package/src/superlocalmemory/retrieval/semantic_channel.py +0 -638
  496. package/src/superlocalmemory/retrieval/spreading.py +0 -288
  497. package/src/superlocalmemory/retrieval/spreading_activation.py +0 -616
  498. package/src/superlocalmemory/retrieval/strategy.py +0 -248
  499. package/src/superlocalmemory/retrieval/temporal_channel.py +0 -433
  500. package/src/superlocalmemory/retrieval/temporal_frame.py +0 -102
  501. package/src/superlocalmemory/retrieval/temporal_utils.py +0 -122
  502. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +0 -499
  503. package/src/superlocalmemory/retrieval/time_window.py +0 -181
  504. package/src/superlocalmemory/retrieval/vector_store.py +0 -863
  505. package/src/superlocalmemory/server/__init__.py +0 -1
  506. package/src/superlocalmemory/server/api.py +0 -310
  507. package/src/superlocalmemory/server/asset_versions.py +0 -171
  508. package/src/superlocalmemory/server/bandit_loops.py +0 -158
  509. package/src/superlocalmemory/server/config_file.py +0 -90
  510. package/src/superlocalmemory/server/consolidation_runner.py +0 -140
  511. package/src/superlocalmemory/server/egress_policy.py +0 -258
  512. package/src/superlocalmemory/server/loopback.py +0 -85
  513. package/src/superlocalmemory/server/middleware/__init__.py +0 -11
  514. package/src/superlocalmemory/server/middleware/security_headers.py +0 -144
  515. package/src/superlocalmemory/server/origin.py +0 -55
  516. package/src/superlocalmemory/server/profile_runtime.py +0 -515
  517. package/src/superlocalmemory/server/rbac_enforce.py +0 -194
  518. package/src/superlocalmemory/server/recall_health.py +0 -343
  519. package/src/superlocalmemory/server/recall_serializer.py +0 -320
  520. package/src/superlocalmemory/server/route_mutations.py +0 -104
  521. package/src/superlocalmemory/server/routes/__init__.py +0 -4
  522. package/src/superlocalmemory/server/routes/abstraction.py +0 -314
  523. package/src/superlocalmemory/server/routes/adapters.py +0 -63
  524. package/src/superlocalmemory/server/routes/agents.py +0 -303
  525. package/src/superlocalmemory/server/routes/backup.py +0 -869
  526. package/src/superlocalmemory/server/routes/behavioral.py +0 -659
  527. package/src/superlocalmemory/server/routes/brain.py +0 -1892
  528. package/src/superlocalmemory/server/routes/chat.py +0 -393
  529. package/src/superlocalmemory/server/routes/compliance.py +0 -533
  530. package/src/superlocalmemory/server/routes/config_api.py +0 -703
  531. package/src/superlocalmemory/server/routes/data_io.py +0 -329
  532. package/src/superlocalmemory/server/routes/entity.py +0 -237
  533. package/src/superlocalmemory/server/routes/events.py +0 -214
  534. package/src/superlocalmemory/server/routes/evolution.py +0 -510
  535. package/src/superlocalmemory/server/routes/helpers.py +0 -499
  536. package/src/superlocalmemory/server/routes/ingest.py +0 -137
  537. package/src/superlocalmemory/server/routes/insights.py +0 -366
  538. package/src/superlocalmemory/server/routes/learning.py +0 -834
  539. package/src/superlocalmemory/server/routes/learning_telemetry.py +0 -154
  540. package/src/superlocalmemory/server/routes/lifecycle.py +0 -184
  541. package/src/superlocalmemory/server/routes/memories.py +0 -1661
  542. package/src/superlocalmemory/server/routes/mesh.py +0 -517
  543. package/src/superlocalmemory/server/routes/mesh_lock.py +0 -54
  544. package/src/superlocalmemory/server/routes/mesh_state.py +0 -63
  545. package/src/superlocalmemory/server/routes/optimize.py +0 -197
  546. package/src/superlocalmemory/server/routes/prewarm.py +0 -173
  547. package/src/superlocalmemory/server/routes/profiles.py +0 -292
  548. package/src/superlocalmemory/server/routes/ratelimit.py +0 -132
  549. package/src/superlocalmemory/server/routes/rbac.py +0 -366
  550. package/src/superlocalmemory/server/routes/stats.py +0 -385
  551. package/src/superlocalmemory/server/routes/tiers.py +0 -222
  552. package/src/superlocalmemory/server/routes/timeline.py +0 -258
  553. package/src/superlocalmemory/server/routes/token.py +0 -90
  554. package/src/superlocalmemory/server/routes/v3_api.py +0 -3023
  555. package/src/superlocalmemory/server/routes/ws.py +0 -171
  556. package/src/superlocalmemory/server/security_middleware.py +0 -89
  557. package/src/superlocalmemory/server/ui.py +0 -354
  558. package/src/superlocalmemory/server/unified_daemon.py +0 -6326
  559. package/src/superlocalmemory/server/write_identity.py +0 -195
  560. package/src/superlocalmemory/storage/__init__.py +0 -0
  561. package/src/superlocalmemory/storage/_migration_internals.py +0 -638
  562. package/src/superlocalmemory/storage/_schema_version.py +0 -174
  563. package/src/superlocalmemory/storage/access_log.py +0 -170
  564. package/src/superlocalmemory/storage/admission_codec.py +0 -129
  565. package/src/superlocalmemory/storage/admission_journal.py +0 -843
  566. package/src/superlocalmemory/storage/agent_experience.py +0 -546
  567. package/src/superlocalmemory/storage/backup.py +0 -531
  568. package/src/superlocalmemory/storage/correction_cases.py +0 -670
  569. package/src/superlocalmemory/storage/database.py +0 -3180
  570. package/src/superlocalmemory/storage/deferred_writes.py +0 -209
  571. package/src/superlocalmemory/storage/embedding_codec.py +0 -200
  572. package/src/superlocalmemory/storage/embedding_migrator.py +0 -672
  573. package/src/superlocalmemory/storage/erasure_fence.py +0 -45
  574. package/src/superlocalmemory/storage/execution_learning.py +0 -285
  575. package/src/superlocalmemory/storage/external_evidence.py +0 -359
  576. package/src/superlocalmemory/storage/generation_fence.py +0 -63
  577. package/src/superlocalmemory/storage/lineage_retention.py +0 -236
  578. package/src/superlocalmemory/storage/logical_edges.py +0 -86
  579. package/src/superlocalmemory/storage/memory_write.py +0 -115
  580. package/src/superlocalmemory/storage/migration_runner.py +0 -895
  581. package/src/superlocalmemory/storage/migration_v33.py +0 -140
  582. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +0 -67
  583. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +0 -107
  584. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +0 -38
  585. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +0 -46
  586. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +0 -75
  587. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +0 -75
  588. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +0 -63
  589. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +0 -94
  590. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +0 -80
  591. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +0 -87
  592. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +0 -72
  593. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +0 -55
  594. package/src/superlocalmemory/storage/migrations/M014_v345_scale_ready.py +0 -45
  595. package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +0 -58
  596. package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +0 -120
  597. package/src/superlocalmemory/storage/migrations/M017_ccq_scope_column.py +0 -79
  598. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +0 -120
  599. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +0 -54
  600. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +0 -52
  601. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +0 -108
  602. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +0 -86
  603. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +0 -194
  604. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +0 -87
  605. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +0 -90
  606. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +0 -136
  607. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +0 -163
  608. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +0 -305
  609. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +0 -137
  610. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +0 -93
  611. package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +0 -80
  612. package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +0 -188
  613. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +0 -148
  614. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +0 -58
  615. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +0 -113
  616. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +0 -107
  617. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +0 -162
  618. package/src/superlocalmemory/storage/migrations/M038_learning_feedback_channel.py +0 -77
  619. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +0 -137
  620. package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +0 -254
  621. package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +0 -189
  622. package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +0 -245
  623. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +0 -512
  624. package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +0 -127
  625. package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +0 -158
  626. package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +0 -620
  627. package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +0 -306
  628. package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +0 -229
  629. package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +0 -201
  630. package/src/superlocalmemory/storage/migrations/M050_execution_learning_v2.py +0 -70
  631. package/src/superlocalmemory/storage/migrations/__init__.py +0 -103
  632. package/src/superlocalmemory/storage/migrations.py +0 -333
  633. package/src/superlocalmemory/storage/models.py +0 -500
  634. package/src/superlocalmemory/storage/projection_outbox.py +0 -346
  635. package/src/superlocalmemory/storage/quantized_store.py +0 -280
  636. package/src/superlocalmemory/storage/read_connection.py +0 -115
  637. package/src/superlocalmemory/storage/retention_policy.py +0 -860
  638. package/src/superlocalmemory/storage/schema.py +0 -1108
  639. package/src/superlocalmemory/storage/schema_code_graph.py +0 -282
  640. package/src/superlocalmemory/storage/schema_v32.py +0 -382
  641. package/src/superlocalmemory/storage/schema_v3410.py +0 -159
  642. package/src/superlocalmemory/storage/schema_v3411.py +0 -149
  643. package/src/superlocalmemory/storage/schema_v343.py +0 -315
  644. package/src/superlocalmemory/storage/schema_v345.py +0 -109
  645. package/src/superlocalmemory/storage/schema_v347.py +0 -140
  646. package/src/superlocalmemory/storage/sqlite_vectors.py +0 -169
  647. package/src/superlocalmemory/storage/v2_migrator.py +0 -466
  648. package/src/superlocalmemory/storage/write_coordinator.py +0 -949
  649. package/src/superlocalmemory/storage/write_lock.py +0 -88
  650. package/src/superlocalmemory/summaries/__init__.py +0 -37
  651. package/src/superlocalmemory/summaries/base.py +0 -267
  652. package/src/superlocalmemory/summaries/daily_reflection.py +0 -340
  653. package/src/superlocalmemory/summaries/non_answer.py +0 -223
  654. package/src/superlocalmemory/summaries/project_work_log.py +0 -440
  655. package/src/superlocalmemory/summaries/session_summary.py +0 -311
  656. package/src/superlocalmemory/trust/__init__.py +0 -0
  657. package/src/superlocalmemory/trust/gate.py +0 -171
  658. package/src/superlocalmemory/trust/provenance.py +0 -124
  659. package/src/superlocalmemory/trust/scorer.py +0 -413
  660. package/src/superlocalmemory/trust/signals.py +0 -153
  661. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +0 -64
  662. package/src/superlocalmemory/ui/assets/slm-icon.svg +0 -36
  663. package/src/superlocalmemory/ui/css/brain.css +0 -409
  664. package/src/superlocalmemory/ui/css/design-system.css +0 -696
  665. package/src/superlocalmemory/ui/css/legacy-dashboard.css +0 -663
  666. package/src/superlocalmemory/ui/css/neural-glass.css +0 -1599
  667. package/src/superlocalmemory/ui/css/od-bridge.css +0 -158
  668. package/src/superlocalmemory/ui/favicon.svg +0 -36
  669. package/src/superlocalmemory/ui/index.html +0 -1638
  670. package/src/superlocalmemory/ui/js/agents.js +0 -192
  671. package/src/superlocalmemory/ui/js/auto-settings.js +0 -624
  672. package/src/superlocalmemory/ui/js/brain.js +0 -1400
  673. package/src/superlocalmemory/ui/js/clusters.js +0 -326
  674. package/src/superlocalmemory/ui/js/compliance.js +0 -307
  675. package/src/superlocalmemory/ui/js/core.js +0 -566
  676. package/src/superlocalmemory/ui/js/dashboard.js +0 -503
  677. package/src/superlocalmemory/ui/js/event-delegation.js +0 -113
  678. package/src/superlocalmemory/ui/js/events.js +0 -178
  679. package/src/superlocalmemory/ui/js/fact-detail.js +0 -142
  680. package/src/superlocalmemory/ui/js/feedback.js +0 -339
  681. package/src/superlocalmemory/ui/js/graph-event-bus.js +0 -83
  682. package/src/superlocalmemory/ui/js/graph-filters.js +0 -220
  683. package/src/superlocalmemory/ui/js/graph-ui.js +0 -214
  684. package/src/superlocalmemory/ui/js/ide-status.js +0 -115
  685. package/src/superlocalmemory/ui/js/init.js +0 -54
  686. package/src/superlocalmemory/ui/js/knowledge-graph.js +0 -945
  687. package/src/superlocalmemory/ui/js/lifecycle.js +0 -387
  688. package/src/superlocalmemory/ui/js/math-health.js +0 -114
  689. package/src/superlocalmemory/ui/js/memories.js +0 -394
  690. package/src/superlocalmemory/ui/js/memory-chat.js +0 -371
  691. package/src/superlocalmemory/ui/js/memory-timeline.js +0 -265
  692. package/src/superlocalmemory/ui/js/modal.js +0 -733
  693. package/src/superlocalmemory/ui/js/ng-entities.js +0 -298
  694. package/src/superlocalmemory/ui/js/ng-health.js +0 -208
  695. package/src/superlocalmemory/ui/js/ng-ingestion.js +0 -203
  696. package/src/superlocalmemory/ui/js/ng-mesh.js +0 -374
  697. package/src/superlocalmemory/ui/js/ng-shell.js +0 -524
  698. package/src/superlocalmemory/ui/js/ng-skills.js +0 -663
  699. package/src/superlocalmemory/ui/js/od-agents.js +0 -588
  700. package/src/superlocalmemory/ui/js/od-auth-gate.js +0 -257
  701. package/src/superlocalmemory/ui/js/od-backup.js +0 -878
  702. package/src/superlocalmemory/ui/js/od-boundedloops.js +0 -324
  703. package/src/superlocalmemory/ui/js/od-brain.js +0 -1095
  704. package/src/superlocalmemory/ui/js/od-compliance-ext.js +0 -301
  705. package/src/superlocalmemory/ui/js/od-components.js +0 -147
  706. package/src/superlocalmemory/ui/js/od-entities.js +0 -622
  707. package/src/superlocalmemory/ui/js/od-graph.js +0 -776
  708. package/src/superlocalmemory/ui/js/od-health.js +0 -579
  709. package/src/superlocalmemory/ui/js/od-mcp.js +0 -508
  710. package/src/superlocalmemory/ui/js/od-memories.js +0 -1499
  711. package/src/superlocalmemory/ui/js/od-mesh.js +0 -645
  712. package/src/superlocalmemory/ui/js/od-operations.js +0 -1268
  713. package/src/superlocalmemory/ui/js/od-ops-health.js +0 -417
  714. package/src/superlocalmemory/ui/js/od-optimize.js +0 -828
  715. package/src/superlocalmemory/ui/js/od-settings.js +0 -1275
  716. package/src/superlocalmemory/ui/js/od-shell.js +0 -819
  717. package/src/superlocalmemory/ui/js/od-skills.js +0 -600
  718. package/src/superlocalmemory/ui/js/od-team.js +0 -265
  719. package/src/superlocalmemory/ui/js/optimize.js +0 -191
  720. package/src/superlocalmemory/ui/js/profiles.js +0 -362
  721. package/src/superlocalmemory/ui/js/quick-actions.js +0 -334
  722. package/src/superlocalmemory/ui/js/recall-lab.js +0 -373
  723. package/src/superlocalmemory/ui/js/search.js +0 -86
  724. package/src/superlocalmemory/ui/js/settings.js +0 -556
  725. package/src/superlocalmemory/ui/js/timeline.js +0 -62
  726. package/src/superlocalmemory/ui/js/trust-dashboard.js +0 -225
  727. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +0 -2018
  728. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  729. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  730. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +0 -7
  731. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +0 -6
  732. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +0 -2
  733. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +0 -2
  734. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +0 -2
  735. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +0 -8
  736. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  737. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  738. package/src/superlocalmemory/ui/vendor/sigma.min.js +0 -1
  739. package/src/superlocalmemory/vector/__init__.py +0 -9
  740. package/src/superlocalmemory/vector/lancedb_backend.py +0 -366
@@ -1,4710 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
-
5
- """CLI command implementations.
6
-
7
- Each function handles one CLI command. Dispatch routes by name.
8
- All data-returning commands support --json for agent-native output.
9
-
10
- Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import logging
16
- import os
17
- import sys
18
- from argparse import Namespace
19
- from pathlib import Path
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- def _cli_projection_queue_depth(db_path: object) -> int:
25
- """Facts queued for the graph and vector projections, read straight from disk.
26
-
27
- The no-daemon branch of ``slm status`` has no engine, so it opens the store
28
- read-only rather than building one. A store that predates the queue reports
29
- zero, which is the truth for it.
30
- """
31
- import sqlite3
32
-
33
- from superlocalmemory.core.status_contract import projection_queue_depth
34
-
35
- try:
36
- conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
37
- except sqlite3.Error:
38
- return 0
39
- try:
40
- return projection_queue_depth(conn)
41
- finally:
42
- conn.close()
43
-
44
-
45
- def _daemon_unavailable(command: str, use_json: bool) -> None:
46
- """Exit a mutation client without opening a process-local writer.
47
-
48
- The bare "owned daemon is unavailable" of earlier releases described a
49
- stopped daemon, a recycled PID, an unreachable port and an identity
50
- mismatch identically, which gave issue #104's reporter nothing to act on.
51
- The diagnosis names the evidence and the next command to run.
52
- """
53
- from superlocalmemory.cli.daemon import describe_daemon_unavailability
54
-
55
- diagnosis = describe_daemon_unavailability()
56
- error = {
57
- "code": "DAEMON_UNAVAILABLE",
58
- "reason": diagnosis["reason"],
59
- "message": f"Owned daemon is unavailable: {diagnosis['message']}",
60
- "hint": diagnosis["hint"],
61
- "retryable": True,
62
- }
63
- if use_json:
64
- from superlocalmemory.cli.json_output import json_print
65
-
66
- json_print(command, error=error)
67
- else:
68
- print(
69
- f"DAEMON_UNAVAILABLE ({diagnosis['reason']}): "
70
- f"{diagnosis['message']} {diagnosis['hint']}",
71
- file=sys.stderr,
72
- )
73
- raise SystemExit(1)
74
-
75
-
76
- def _cmd_db_dispatch(args: Namespace) -> None:
77
- """Route ``slm db ...`` subcommands. LLD-06 §7.2."""
78
- sub = getattr(args, "db_command", None)
79
- if sub == "migrate":
80
- from superlocalmemory.cli.db_migrate import cmd_db_migrate
81
- rc = cmd_db_migrate(args)
82
- if rc:
83
- sys.exit(rc)
84
- return
85
- if sub == "scale":
86
- from superlocalmemory.cli.scale_engine_cmd import cmd_db_scale
87
- rc = cmd_db_scale(args)
88
- if rc:
89
- sys.exit(rc)
90
- return
91
- if sub == "reembed":
92
- _cmd_db_reembed(args)
93
- return
94
- if sub == "regraph":
95
- _cmd_db_regraph(args)
96
- return
97
- print(
98
- "Usage: slm db migrate [--status] [--dry-run] "
99
- "| slm db scale <action> "
100
- "| slm db regraph [--check] [--profile NAME] "
101
- "| slm db reembed [--missing-only] [--all-profiles] [--limit N]"
102
- )
103
- sys.exit(2)
104
-
105
-
106
- def _cmd_db_regraph(args: Namespace) -> None:
107
- """Re-derive the graph copy of your memories from the store.
108
-
109
- Your memories live in one place; a second copy of the connections between
110
- them is kept in a graph store so that searching them is fast. The two are
111
- kept in step by a queue. If the queue is empty and the two still disagree
112
- -- after a crash, a disk problem, or a copy restored from elsewhere --
113
- nothing notices, because "the queue is empty" is what "in step" looks like.
114
-
115
- This is how you say so out loud. ``--check`` reports the difference and
116
- changes nothing; without it, every memory is queued to be re-derived and
117
- the background worker rebuilds the copy from the store, which is the one
118
- that is authoritative.
119
- """
120
- from superlocalmemory.core.config import SLMConfig
121
- from superlocalmemory.storage import projection_outbox
122
- from superlocalmemory.storage.database import DatabaseManager
123
- from superlocalmemory.storage.logical_edges import count_logical_edges
124
-
125
- config = SLMConfig.load()
126
- db = DatabaseManager(config.db_path)
127
- wanted = str(getattr(args, "profile", "") or "").strip()
128
- try:
129
- rows = db.execute(
130
- "SELECT DISTINCT profile_id FROM atomic_facts ORDER BY profile_id"
131
- )
132
- profiles = [dict(r)["profile_id"] for r in rows]
133
- except Exception as exc: # noqa: BLE001
134
- print(f"[slm] could not list workspaces: {exc}")
135
- sys.exit(1)
136
- if wanted:
137
- profiles = [p for p in profiles if p == wanted]
138
- if not profiles:
139
- print(f"[slm] no workspace named {wanted!r}")
140
- sys.exit(1)
141
-
142
- backend = None
143
- try:
144
- from superlocalmemory.core.backend_orchestrator import get_orchestrator
145
-
146
- orchestrator = get_orchestrator()
147
- backend = orchestrator.get_graph_backend() if orchestrator else None
148
- except Exception as exc: # noqa: BLE001
149
- print(f"[slm] no graph copy is configured on this store ({exc}).")
150
- return
151
- if backend is None:
152
- print("[slm] no graph copy is configured on this store; nothing to do.")
153
- return
154
-
155
- total_queued = 0
156
- for profile_id in profiles:
157
- with db.raw_connection() as conn:
158
- stored = count_logical_edges(conn, profile_id)
159
- try:
160
- result = backend._db.run(
161
- "?[count(a)] := *edge{from_id: a, profile_id: $pid}",
162
- {"pid": profile_id},
163
- )
164
- copied = int(result.values.tolist()[0][0]) if len(result) else 0
165
- except Exception as exc: # noqa: BLE001
166
- print(f" {profile_id}: the graph copy could not be read ({exc})")
167
- continue
168
- drift = abs(copied - stored) / float(stored) if stored else 0.0
169
- pending = 0
170
- try:
171
- pending_rows = db.execute(
172
- "SELECT COUNT(*) AS c FROM projection_outbox WHERE profile_id = ?",
173
- (profile_id,),
174
- )
175
- pending = int(dict(pending_rows[0])["c"]) if pending_rows else 0
176
- except Exception: # noqa: BLE001
177
- pending = -1
178
- print(
179
- f" {profile_id}: {stored} connections stored, {copied} in the "
180
- f"graph copy ({drift:.1%} apart), {pending} waiting to be applied"
181
- )
182
- if getattr(args, "check", False):
183
- continue
184
-
185
- fact_rows = db.execute(
186
- "SELECT fact_id FROM atomic_facts WHERE profile_id = ?", (profile_id,)
187
- )
188
- fact_ids = [dict(r)["fact_id"] for r in fact_rows]
189
- if not fact_ids:
190
- continue
191
- if not projection_outbox.is_available(db):
192
- print(" (this store has no queue to rebuild through)")
193
- continue
194
- for start in range(0, len(fact_ids), 500):
195
- projection_outbox.enqueue_many(
196
- db, fact_ids[start:start + 500], profile_id,
197
- op=projection_outbox.OP_UPSERT,
198
- )
199
- total_queued += len(fact_ids)
200
- print(f" {profile_id}: queued {len(fact_ids)} memories to re-derive")
201
-
202
- db.close()
203
- if getattr(args, "check", False):
204
- return
205
- if total_queued:
206
- print(
207
- f"[slm] {total_queued} memories queued. The background worker "
208
- f"rebuilds the copy from the store; watch `slm status` until the "
209
- f"waiting count reaches zero."
210
- )
211
- else:
212
- print("[slm] nothing to re-derive.")
213
-
214
-
215
- def _cmd_db_reembed(args: Namespace) -> None:
216
- """Backfill NULL embeddings on atomic_facts.
217
-
218
- Scans for atomic_facts rows with no embedding (facts stored while the
219
- embedder was unavailable) and embeds them now. Safe to re-run — only
220
- NULL rows are processed.
221
-
222
- Usage:
223
- slm db reembed --missing-only # backfill NULLs (default)
224
- slm db reembed --missing-only --limit 500 # cap per run
225
- slm db reembed --missing-only --all-profiles # cover every profile
226
- """
227
- import time
228
-
229
- from superlocalmemory.core.config import SLMConfig
230
- from superlocalmemory.core.engine import MemoryEngine
231
- from superlocalmemory.storage.embedding_migrator import backfill_missing_embeddings
232
-
233
- use_json = getattr(args, "json", False)
234
- missing_only = getattr(args, "missing_only", True)
235
- all_profiles = getattr(args, "all_profiles", False)
236
- limit = getattr(args, "limit", None)
237
-
238
- if not missing_only:
239
- if use_json:
240
- from superlocalmemory.cli.json_output import json_print
241
- json_print(
242
- "db-reembed",
243
- error={
244
- "code": "NOT_IMPLEMENTED",
245
- "message": "Only --missing-only is supported. "
246
- "For full re-embed use 'slm db migrate'.",
247
- },
248
- )
249
- else:
250
- print("Only --missing-only mode is supported. "
251
- "For full re-embed use 'slm db migrate'.")
252
- sys.exit(2)
253
-
254
- try:
255
- config = SLMConfig.load()
256
- engine = MemoryEngine(config)
257
- engine.initialize()
258
- except Exception as exc:
259
- if use_json:
260
- from superlocalmemory.cli.json_output import json_print
261
- json_print(
262
- "db-reembed",
263
- error={"code": "ENGINE_INIT_ERROR", "message": str(exc)},
264
- )
265
- else:
266
- print(f"Error initializing engine: {exc}", file=sys.stderr)
267
- sys.exit(1)
268
-
269
- embedder = getattr(engine, "_embedder", None)
270
- if embedder is None:
271
- if use_json:
272
- from superlocalmemory.cli.json_output import json_print
273
- json_print(
274
- "db-reembed",
275
- error={
276
- "code": "NO_EMBEDDER",
277
- "message": "Embedder is not available. "
278
- "Start the daemon first or check embedding configuration.",
279
- },
280
- )
281
- else:
282
- print(
283
- "Embedder is not available. "
284
- "Start the daemon ('slm serve') or check embedding config.",
285
- file=sys.stderr,
286
- )
287
- engine.close()
288
- sys.exit(1)
289
-
290
- t0 = time.monotonic()
291
- try:
292
- result = backfill_missing_embeddings(
293
- config,
294
- engine._db,
295
- embedder,
296
- batch_size=50,
297
- limit=limit,
298
- all_profiles=all_profiles,
299
- )
300
- except Exception as exc:
301
- if use_json:
302
- from superlocalmemory.cli.json_output import json_print
303
- json_print(
304
- "db-reembed",
305
- error={"code": "BACKFILL_ERROR", "message": str(exc)},
306
- )
307
- else:
308
- print(f"Backfill error: {exc}", file=sys.stderr)
309
- engine.close()
310
- sys.exit(1)
311
-
312
- elapsed = time.monotonic() - t0
313
- engine.close()
314
-
315
- if use_json:
316
- from superlocalmemory.cli.json_output import json_print
317
- json_print(
318
- "db-reembed",
319
- data={
320
- "scanned": result["scanned"],
321
- "embedded": result["embedded"],
322
- "remaining_null": result["remaining_null"],
323
- "elapsed_s": round(elapsed, 2),
324
- },
325
- )
326
- else:
327
- print(
328
- f"Backfill complete — "
329
- f"scanned={result['scanned']} "
330
- f"embedded={result['embedded']} "
331
- f"remaining_null={result['remaining_null']} "
332
- f"elapsed={elapsed:.1f}s"
333
- )
334
-
335
-
336
- def _cmd_mesh_dispatch(args: Namespace) -> None:
337
- """Route ``slm mesh ...`` inspection subcommands (M-03)."""
338
- from superlocalmemory.cli.mesh_cmd import cmd_mesh
339
- rc = cmd_mesh(args)
340
- if rc:
341
- sys.exit(rc)
342
-
343
-
344
- def _cmd_escape_disable(args: Namespace) -> None:
345
- from superlocalmemory.cli.escape_hatch import cmd_disable
346
- cmd_disable(args)
347
-
348
-
349
- def _cmd_escape_enable(args: Namespace) -> None:
350
- from superlocalmemory.cli.escape_hatch import cmd_enable
351
- cmd_enable(args)
352
-
353
-
354
- def _cmd_escape_clear_cache(args: Namespace) -> None:
355
- from superlocalmemory.cli.escape_hatch import cmd_clear_cache
356
- cmd_clear_cache(args)
357
-
358
-
359
- def _cmd_escape_reconfigure(args: Namespace) -> None:
360
- from superlocalmemory.cli.escape_hatch import cmd_reconfigure
361
- cmd_reconfigure(args)
362
-
363
-
364
- def _cmd_escape_benchmark(args: Namespace) -> None:
365
- from superlocalmemory.cli.escape_hatch import cmd_benchmark
366
- cmd_benchmark(args)
367
-
368
-
369
- def _cmd_escape_rotate_token(args: Namespace) -> None:
370
- """S-M07: rotate the install token."""
371
- from superlocalmemory.cli.escape_hatch import cmd_rotate_token
372
- cmd_rotate_token(args)
373
-
374
-
375
- # ---- SLM v3.6 Optimize dispatch functions (additive) ----
376
-
377
- def _cmd_optimize(args: Namespace) -> None:
378
- from superlocalmemory.cli.optimize_cmd import cmd_optimize
379
- cmd_optimize(args)
380
-
381
-
382
- def _cmd_cache(args: Namespace) -> None:
383
- from superlocalmemory.cli.cache_cmd import cmd_cache
384
- cmd_cache(args)
385
-
386
-
387
- def _cmd_compress(args: Namespace) -> None:
388
- from superlocalmemory.cli.compress_cmd import cmd_compress
389
- cmd_compress(args)
390
-
391
-
392
- def _cmd_proxy(args: Namespace) -> None:
393
- from superlocalmemory.cli.proxy_cmd import cmd_proxy
394
- cmd_proxy(args)
395
-
396
-
397
- def _cmd_help_optimize(args: Namespace) -> None:
398
- from superlocalmemory.cli.help_cmd import cmd_help_optimize
399
- cmd_help_optimize(args)
400
-
401
-
402
- def _cmd_loop(args: Namespace) -> None:
403
- from superlocalmemory.cli.loop_cmd import cmd_loop
404
- cmd_loop(args)
405
-
406
-
407
- def _cmd_ops(args: Namespace) -> None:
408
- """Operational recovery & admin remediation commands."""
409
- from superlocalmemory.cli.ops_cmd import cmd_ops
410
- cmd_ops(args)
411
-
412
-
413
- def _cmd_gdpr_dispatch(args: Namespace) -> None:
414
- """V4.0.6: GDPR subject-rights CLI (Art.15/17/20)."""
415
- from superlocalmemory.cli.gdpr_cmd import cmd_gdpr
416
- cmd_gdpr(args)
417
-
418
-
419
- def _cmd_summary_dispatch(args: Namespace) -> None:
420
- """V4.0.7: readable session/day/project summaries (issue #113)."""
421
- from superlocalmemory.cli.summary_cmd import cmd_summary
422
- cmd_summary(args)
423
-
424
-
425
- # ---- end SLM v3.6 Optimize dispatch functions ----
426
-
427
-
428
- def cmd_session(args: Namespace) -> None:
429
- """#49: Open/close a session locally via the daemon — no model roundtrip,
430
- so a shell hook (e.g. Claude session-start / /quit) can call it directly.
431
- """
432
- from superlocalmemory.cli.daemon import (
433
- daemon_request, ensure_daemon, is_daemon_running,
434
- )
435
-
436
- action = getattr(args, "session_command", None)
437
- if action not in ("open", "close"):
438
- print("Usage: slm session {open|close} "
439
- "[--session-id ID] [--project-path PATH] [--query Q]")
440
- return
441
-
442
- if not is_daemon_running():
443
- ensure_daemon()
444
-
445
- if action == "open":
446
- body = {
447
- "project_path": getattr(args, "project_path", "") or "",
448
- "query": getattr(args, "query", "") or "",
449
- "max_results": int(getattr(args, "max_results", 10) or 10),
450
- }
451
- resp = daemon_request("POST", "/session/open", body)
452
- if resp and resp.get("ok"):
453
- print(f"Session opened — warmed {resp.get('warmed', 0)} memories "
454
- f"(query: {resp.get('query', '')})")
455
- else:
456
- print("Session open failed (daemon unreachable?)")
457
- return
458
-
459
- # close
460
- body = {"session_id": getattr(args, "session_id", "") or ""}
461
- resp = daemon_request("POST", "/session/close", body)
462
- if resp and resp.get("ok"):
463
- sid = resp.get("session_id") or "(most recent)"
464
- print(f"Session closed: {sid} — "
465
- f"{resp.get('summary_events_created', 0)} summary event(s) created")
466
- else:
467
- print("Session close failed (daemon unreachable?)")
468
-
469
-
470
- def _cmd_upgrade_hosts(args: Namespace) -> None:
471
- """Run the consented host-integration refresh command."""
472
- from superlocalmemory.cli.host_upgrades import cmd_upgrade_hosts
473
-
474
- cmd_upgrade_hosts(args)
475
-
476
-
477
- def dispatch(args: Namespace) -> None:
478
- """Route CLI command to the appropriate handler."""
479
- handlers = {
480
- "init": cmd_init,
481
- "setup": cmd_setup,
482
- "mode": cmd_mode,
483
- "provider": cmd_provider,
484
- "connect": cmd_connect,
485
- "upgrade-hosts": _cmd_upgrade_hosts,
486
- "migrate": cmd_migrate,
487
- "list": cmd_list,
488
- "remember": cmd_remember,
489
- "recall": cmd_recall,
490
- "search": cmd_recall, # v3.6.12 (parity-3): MCP exposes a `search` verb; give the CLI parity (recall is multi-channel incl. BM25/keyword).
491
- "forget": cmd_forget,
492
- "delete": cmd_delete,
493
- "update": cmd_update,
494
- "review-correction": cmd_review_correction,
495
- "status": cmd_status,
496
- "brain": cmd_brain,
497
- "health": cmd_health,
498
- "doctor": cmd_doctor,
499
- "trace": cmd_trace,
500
- "mcp": cmd_mcp,
501
- "warmup": cmd_warmup,
502
- "dashboard": cmd_dashboard,
503
- "profile": cmd_profile,
504
- "hooks": cmd_hooks,
505
- "codex": cmd_codex,
506
- "session-context": cmd_session_context,
507
- "session": cmd_session, # #49: local session open/close for hooks
508
- "observe": cmd_observe,
509
- # V3.3 commands
510
- "decay": cmd_decay,
511
- "quantize": cmd_quantize,
512
- "consolidate": cmd_consolidate,
513
- "soft-prompts": cmd_soft_prompts,
514
- "reap": cmd_reap,
515
- # V3.3.21 daemon
516
- "serve": cmd_serve,
517
- # V3.4.9 nuclear restart
518
- "restart": cmd_restart,
519
- # V3.4.3 ingestion adapters
520
- "adapters": cmd_adapters,
521
- # V3.4.8 external observation ingestion
522
- "ingest": cmd_ingest,
523
- # V3.4.11 skill evolution
524
- "config": cmd_config,
525
- "evolve": cmd_evolve,
526
- # V3.4.22 LLD-05 context pre-staging
527
- "context": _cmd_context_dispatch,
528
- # V3.4.22 LLD-06 additive schema migrations
529
- "db": _cmd_db_dispatch,
530
- # V3.7.9 M-03 — terminal mesh inspection
531
- "mesh": _cmd_mesh_dispatch,
532
- # V3.4.22 Stage 8 SB-5 — MASTER-PLAN §8 escape hatches.
533
- "disable": _cmd_escape_disable,
534
- "enable": _cmd_escape_enable,
535
- "clear-cache": _cmd_escape_clear_cache,
536
- "reconfigure": _cmd_escape_reconfigure,
537
- "benchmark": _cmd_escape_benchmark,
538
- "rotate-token": _cmd_escape_rotate_token,
539
- "evidence": _cmd_evidence,
540
- "diagnostics": _cmd_diagnostics,
541
- # LLD-06 — `slm wrap <agent> [args...]` activates the Optimize proxy.
542
- "wrap": _cmd_wrap,
543
- # V3.6 Optimize subcommands (additive)
544
- "optimize": _cmd_optimize,
545
- "cache": _cmd_cache,
546
- "compress": _cmd_compress,
547
- "proxy": _cmd_proxy,
548
- "help-optimize": _cmd_help_optimize,
549
- # V3.8.0 bounded loops (gate-verified agent loops, SLM-backed ledger)
550
- "loop": _cmd_loop,
551
- # V3.8.2 super-help — grouped overview of every command + topics
552
- "help": cmd_help,
553
- # Operational recovery & admin remediation
554
- "ops": _cmd_ops,
555
- # V4.0.6: GDPR subject-rights CLI (Art.15/17/20)
556
- "gdpr": _cmd_gdpr_dispatch,
557
- "summary": _cmd_summary_dispatch,
558
- }
559
- handler = handlers.get(args.command)
560
- if handler:
561
- from superlocalmemory.cli.daemon import DaemonRefused
562
-
563
- try:
564
- handler(args)
565
- except DaemonRefused as refusal:
566
- # One place, because a command added next year will not remember to
567
- # do this. A refusal is an answer: say so and stop, rather than
568
- # printing a traceback or -- worse -- letting a command fall back to
569
- # writing locally what the workspace has just declined.
570
- print(
571
- f"[slm] {refusal}. This workspace requires authentication; "
572
- "set SLM_USER_SESSION or log in, then try again.",
573
- flush=True,
574
- )
575
- sys.exit(1)
576
- else:
577
- print(f"Unknown command: {args.command}")
578
- sys.exit(1)
579
-
580
-
581
- def _cmd_evidence(args: Namespace) -> None:
582
- """Lazy-load the evidence/rebuild command surface."""
583
- from superlocalmemory.cli.evidence_cmd import cmd_evidence
584
-
585
- cmd_evidence(args)
586
-
587
-
588
- def _cmd_diagnostics(args: Namespace) -> None:
589
- """Lazy-load the local aggregate diagnostics export surface."""
590
- from superlocalmemory.cli.diagnostics_cmd import cmd_diagnostics
591
-
592
- cmd_diagnostics(args)
593
-
594
-
595
- def _cmd_wrap(args: Namespace) -> None:
596
- """LLD-06 §6.6 — `slm wrap <agent> [args...]` activates the Optimize proxy.
597
-
598
- Routes the Optimize layer's per-agent launch/activation. See
599
- optimize.adapters._agent_registry for the full agent table.
600
- """
601
- if getattr(args, "list", False):
602
- from superlocalmemory.optimize.adapters.wrap import list_agents
603
- from superlocalmemory.optimize.adapters._agent_registry import AGENT_REGISTRY
604
- print("Registered agents for `slm wrap`:")
605
- for key in list_agents():
606
- spec = AGENT_REGISTRY.get(key, {})
607
- mech = spec.get("mechanism", "unknown")
608
- print(f" {key:20s} mechanism={mech}")
609
- return
610
-
611
- agent = getattr(args, "agent", None)
612
- if not agent:
613
- print("Usage: slm wrap <agent> [args...]\n"
614
- " slm wrap --list\n"
615
- " slm wrap --help")
616
- sys.exit(2)
617
-
618
- agent_args = list(getattr(args, "agent_args", []) or [])
619
- persistent = bool(getattr(args, "persistent", False))
620
- dry_run = bool(getattr(args, "dry_run", False))
621
-
622
- from superlocalmemory.optimize.adapters.wrap import wrap_agent
623
- rc = wrap_agent(agent, agent_args, persistent=persistent, dry_run=dry_run)
624
- if rc:
625
- sys.exit(rc)
626
-
627
-
628
- # -- Daemon serve mode (V3.3.21) ------------------------------------------
629
-
630
- def cmd_serve(args: Namespace) -> None:
631
- """Start/stop the SLM daemon for instant CLI response."""
632
- from superlocalmemory.cli.daemon import is_daemon_running, ensure_daemon, stop_daemon
633
-
634
- action = getattr(args, 'action', 'start')
635
-
636
- if action == 'stop':
637
- if stop_daemon():
638
- print("Daemon stopped.")
639
- else:
640
- print("Daemon was not running.")
641
- return
642
-
643
- if action == 'status':
644
- if is_daemon_running():
645
- from superlocalmemory.cli.daemon import daemon_request
646
- status = daemon_request("GET", "/status")
647
- if status:
648
- print(f"Daemon: RUNNING (PID {status['pid']}, "
649
- f"mode={status['mode']}, facts={status['fact_count']}, "
650
- f"uptime={status['uptime_s']}s, idle={status['idle_s']}s)")
651
- else:
652
- print("Daemon: RUNNING (could not get status)")
653
- else:
654
- print("Daemon: NOT RUNNING")
655
- # Also show OS service status
656
- try:
657
- from superlocalmemory.cli.service_installer import service_status
658
- svc = service_status()
659
- installed = svc.get("installed", False)
660
- print(f"OS Service: {'INSTALLED' if installed else 'NOT INSTALLED'} "
661
- f"({svc.get('service_type', svc.get('platform', '?'))})")
662
- except Exception:
663
- pass
664
- return
665
-
666
- if action == 'install':
667
- # Install OS-level service for auto-start on boot/login
668
- from superlocalmemory.cli.service_installer import install_service
669
- print("Installing SLM as OS service (auto-start on login)...")
670
- if install_service():
671
- print("Service installed \u2713 — SLM will auto-start on login.")
672
- print(" slm serve status — check service status")
673
- print(" slm serve uninstall — remove auto-start")
674
- else:
675
- print("Failed to install service. Check logs.")
676
- return
677
-
678
- if action == 'uninstall':
679
- from superlocalmemory.cli.service_installer import uninstall_service
680
- if uninstall_service():
681
- print("OS service removed \u2713 — SLM will no longer auto-start.")
682
- else:
683
- print("Failed to remove service.")
684
- return
685
-
686
- # Default: start
687
- if is_daemon_running():
688
- print("Daemon already running.")
689
- return
690
-
691
- print("Starting SLM daemon (engine warming up)...")
692
- if ensure_daemon():
693
- print("Daemon started \u2713 — CLI commands are now instant.")
694
- print(" slm serve status — check daemon status")
695
- print(" slm serve stop — stop daemon and free RAM")
696
- else:
697
- from superlocalmemory.infra.data_root import state_path
698
- print(f"Failed to start daemon. Check {state_path('logs', 'daemon.log')}")
699
- # INT-H-02: exit non-zero so callers can detect the failure. The plugin
700
- # launcher (plugin/scripts/slm-launch) guards on this exit code and
701
- # refuses to open a direct MCP writer against a broken daemon; without
702
- # the non-zero exit that guard was a dead no-op.
703
- sys.exit(1)
704
-
705
-
706
- # -- Ingestion Adapters (V3.4.3) ------------------------------------------
707
-
708
-
709
- def cmd_restart(args: Namespace) -> None:
710
- """Restart the one daemon owned by the current SLM data namespace.
711
-
712
- 5-step pipeline:
713
- 1. Capability-stop the owned daemon and its children
714
- 2. Acquire the namespace start lock
715
- 3. Start fresh daemon
716
- 4. Wait for engine warmup + verify health
717
- 5. Optionally open dashboard
718
- """
719
- import time
720
- from superlocalmemory.infra.daemon_identity import canonical_data_root
721
-
722
- use_json = getattr(args, "json", False)
723
- open_dashboard = getattr(args, "dashboard", False)
724
- slm_dir = canonical_data_root()
725
- steps: list[dict] = []
726
-
727
- def _log(step: int, name: str, status: str, detail: str = ""):
728
- entry = {"step": step, "name": name, "status": status, "detail": detail}
729
- steps.append(entry)
730
- if not use_json:
731
- icon = {"ok": "+", "warn": "!", "fail": "x"}.get(status, " ")
732
- print(f" [{icon}] Step {step}: {name}" + (f" — {detail}" if detail else ""))
733
-
734
- if not use_json:
735
- print()
736
- print(" SLM Full System Restart")
737
- print(" " + "=" * 40)
738
- print()
739
-
740
- # Acquire the namespace lock before requesting shutdown. Otherwise an
741
- # auto-starting hook can observe the brief offline window and start a
742
- # second daemon while this command is still waiting for the old process.
743
- _LOCK_FILE = slm_dir / "daemon.lock"
744
- _LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
745
- restart_lock_fd = None
746
- try:
747
- restart_lock_fd = open(_LOCK_FILE, "w")
748
- if sys.platform != "win32":
749
- import fcntl
750
- fcntl.flock(restart_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
751
- except Exception:
752
- pass # Best-effort — don't block restart if lock fails
753
-
754
- # Step 1: stop only the descriptor-owned daemon. Its graceful shutdown
755
- # owns worker termination; process-name-wide scans are forbidden.
756
- from superlocalmemory.cli.daemon import (
757
- owned_daemon_process_alive,
758
- read_descriptor,
759
- stop_daemon,
760
- wait_for_owned_daemon_shutdown,
761
- )
762
-
763
- # v4.1.4: process liveness, not HTTP health-readiness, decides whether a
764
- # stop is owed. is_daemon_running() additionally requires a live /health
765
- # response, which a daemon busy inside /maintenance/run or
766
- # /consolidate/cognitive cannot give for the duration of that call even
767
- # though it is fully alive and holding the port — see stop_daemon()'s
768
- # docstring for the reproduced failure this fixes.
769
- was_running = owned_daemon_process_alive()
770
- owned_descriptor = read_descriptor() if was_running else None
771
- stopped = stop_daemon() if was_running else True
772
- if stopped and was_running:
773
- stopped = wait_for_owned_daemon_shutdown(owned_descriptor)
774
- killed = 1 if was_running and stopped else 0
775
- _log(
776
- 1,
777
- "Stop owned SLM daemon",
778
- "ok" if stopped else "fail",
779
- "owned daemon stopped" if killed else (
780
- "already stopped" if stopped else "owned daemon did not stop"
781
- ),
782
- )
783
- if not stopped:
784
- if restart_lock_fd:
785
- restart_lock_fd.close()
786
- if use_json:
787
- from superlocalmemory.cli.json_output import json_print
788
- json_print(
789
- "restart",
790
- data={"steps": steps, "success": False},
791
- next_actions=[{
792
- "command": "slm doctor",
793
- "description": "Diagnose the owned daemon",
794
- }],
795
- )
796
- else:
797
- print("\n Restart FAILED at step 1. The owned daemon was not stopped.")
798
- return
799
-
800
- # Step 2: the namespace lock was acquired before shutdown so hooks cannot
801
- # auto-start another daemon inside the offline transition.
802
- _log(2, "Acquire namespace start lock", "ok", str(_LOCK_FILE))
803
-
804
- # Step 3: Start fresh daemon (lock still held — no races)
805
- # v3.4.42: Call _start_daemon_subprocess() directly instead of
806
- # ensure_daemon(). The latter tries to acquire daemon.lock itself,
807
- # which the SAME PROCESS holds via restart_lock_fd above — BSD-style
808
- # flock blocks per-fd even within one process, so ensure_daemon would
809
- # fall into its lock-fail branch and time out after 60s while the
810
- # actual daemon never gets started. Calling the helper directly
811
- # bypasses that self-deadlock and starts the daemon as intended.
812
- from superlocalmemory.cli.daemon import _start_daemon_subprocess
813
- started = _start_daemon_subprocess()
814
-
815
- # Release restart lock — daemon is now running with its own lock
816
- if restart_lock_fd:
817
- try:
818
- restart_lock_fd.close()
819
- except Exception:
820
- pass
821
- _log(3, "Start fresh daemon", "ok" if started else "fail",
822
- "daemon started" if started else "failed to start — check slm doctor")
823
-
824
- if not started:
825
- if use_json:
826
- from superlocalmemory.cli.json_output import json_print
827
- json_print("restart", data={"steps": steps, "success": False},
828
- next_actions=[{"command": "slm doctor", "description": "Diagnose issues"}])
829
- else:
830
- print("\n Restart FAILED at step 3. Run: slm doctor")
831
- return
832
-
833
- # Step 4: Wait for warmup + verify health
834
- if not use_json:
835
- print(" [ ] Step 4: Waiting for engine warmup (up to 30s)...", end="", flush=True)
836
-
837
- health = None
838
- engine_ok = False
839
- for attempt in range(15):
840
- time.sleep(2)
841
- try:
842
- from superlocalmemory.cli.daemon import daemon_request
843
- health = daemon_request("GET", "/health")
844
- if health and health.get("engine") == "initialized":
845
- engine_ok = True
846
- break
847
- except Exception:
848
- pass
849
-
850
- if not use_json:
851
- print("\r", end="") # Clear the waiting line
852
-
853
- if engine_ok:
854
- version = health.get("version", "?")
855
- pid = health.get("pid", "?")
856
- _log(4, "Engine health verified", "ok", f"v{version}, PID {pid}, engine=initialized")
857
- else:
858
- engine_state = health.get("engine", "unknown") if health else "unreachable"
859
- _log(4, "Engine health check", "warn",
860
- f"engine={engine_state} — may still be warming up. Try again in 30s.")
861
-
862
- # Step 5: Database integrity check
863
- try:
864
- from superlocalmemory.storage.memory_write import memory_read
865
-
866
- db_path = slm_dir / "memory.db"
867
- if db_path.exists():
868
- with memory_read(db_path) as conn:
869
- integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
870
- fact_count = conn.execute("SELECT COUNT(*) FROM atomic_facts").fetchone()[0]
871
- entity_count = conn.execute("SELECT COUNT(*) FROM canonical_entities").fetchone()[0]
872
- _log(5, "Database integrity", "ok" if integrity == "ok" else "fail",
873
- f"integrity={integrity}, {fact_count} facts, {entity_count} entities")
874
- else:
875
- _log(5, "Database check", "warn", "no database yet — will create on first use")
876
- except Exception as exc:
877
- _log(5, "Database check", "warn", str(exc))
878
-
879
- # Step 6 (optional): Open dashboard
880
- if open_dashboard:
881
- try:
882
- import webbrowser
883
- from superlocalmemory.cli.daemon import _get_port
884
- port = _get_port()
885
- url = f"http://localhost:{port}"
886
- webbrowser.open(url)
887
- _log(6, "Dashboard opened", "ok", url)
888
- except Exception as exc:
889
- _log(6, "Dashboard open", "fail", str(exc))
890
-
891
- # Summary
892
- all_ok = all(s["status"] == "ok" for s in steps)
893
-
894
- if use_json:
895
- from superlocalmemory.cli.json_output import json_print
896
- json_print("restart", data={
897
- "steps": steps, "success": all_ok,
898
- "processes_killed": killed,
899
- "version": health.get("version") if health else None,
900
- }, next_actions=[
901
- {"command": "slm serve status", "description": "Check daemon status"},
902
- {"command": "slm dashboard", "description": "Open dashboard"},
903
- ])
904
- return
905
-
906
- print()
907
- if all_ok:
908
- print(" All systems operational.")
909
- else:
910
- warnings = [s for s in steps if s["status"] != "ok"]
911
- print(f" {len(warnings)} issue(s) — check details above.")
912
- print()
913
-
914
-
915
- def cmd_ingest(args: Namespace) -> None:
916
- """Import external observations into SLM learning pipeline."""
917
- from superlocalmemory.cli.ingest_cmd import cmd_ingest as _ingest
918
- _ingest(args)
919
-
920
-
921
- # -- Config & Evolution (V3.4.11) -----------------------------------------------
922
-
923
-
924
- def cmd_config(args: Namespace) -> None:
925
- """Get or set config values (dot-notation).
926
-
927
- Usage:
928
- slm config set evolution.enabled true
929
- slm config set evolution.backend auto
930
- slm config get evolution.enabled
931
- slm config get evolution.backend
932
- """
933
- import json
934
-
935
- from superlocalmemory.infra.data_root import state_path
936
-
937
- use_json = getattr(args, "json", False)
938
- action = getattr(args, "action", "get")
939
- key = getattr(args, "key", "")
940
- value = getattr(args, "value", None)
941
-
942
- config_path = state_path("config.json")
943
-
944
- # Read existing config
945
- cfg: dict = {}
946
- if config_path.exists():
947
- try:
948
- cfg = json.loads(config_path.read_text())
949
- except (json.JSONDecodeError, OSError):
950
- pass
951
-
952
- if action == "get":
953
- # Parse dot-notation key (e.g. "evolution.enabled")
954
- parts = key.split(".") if key else []
955
- node = cfg
956
- for part in parts:
957
- if isinstance(node, dict) and part in node:
958
- node = node[part]
959
- else:
960
- node = None
961
- break
962
-
963
- if use_json:
964
- from superlocalmemory.cli.json_output import json_print
965
- json_print("config", data={"key": key, "value": node})
966
- else:
967
- if node is None:
968
- print(f"{key}: (not set)")
969
- else:
970
- print(f"{key}: {node}")
971
-
972
- elif action == "set":
973
- _ALLOWED_CONFIG_KEYS = {
974
- "evolution.enabled", "evolution.backend", "evolution.max_evolutions_per_cycle",
975
- "evolution.mutation_model", "evolution.verify_model",
976
- "evolution.confirm_model",
977
- "mesh_enabled", "daemon_idle_timeout", "entity_compilation_enabled",
978
- "graph_backend", "vector_backend",
979
- # scale_engine_state is deliberately NOT settable. It is a record of
980
- # what has been done to the store, not a preference: writing
981
- # "promoted" by hand makes the daemon skip projecting a store that
982
- # was never projected, and writing "local_core" onto a promoted one
983
- # makes it project again over live backends. A real installation was
984
- # found declaring two backends it had never built.
985
- "scope.default_scope", "scope.recall_include_global",
986
- "scope.recall_include_shared",
987
- }
988
- if key not in _ALLOWED_CONFIG_KEYS:
989
- if use_json:
990
- from superlocalmemory.cli.json_output import json_print
991
- json_print("config", error={
992
- "code": "DISALLOWED_KEY",
993
- "message": f"'{key}' is not a configurable key. Allowed: {', '.join(sorted(_ALLOWED_CONFIG_KEYS))}",
994
- })
995
- else:
996
- print(f"Error: '{key}' is not a configurable key. Allowed: {', '.join(sorted(_ALLOWED_CONFIG_KEYS))}")
997
- sys.exit(1)
998
-
999
- if not key or value is None:
1000
- if use_json:
1001
- from superlocalmemory.cli.json_output import json_print
1002
- json_print("config", error={
1003
- "code": "INVALID_INPUT",
1004
- "message": "Usage: slm config set <key> <value>",
1005
- })
1006
- else:
1007
- print("Usage: slm config set <key> <value>")
1008
- sys.exit(1)
1009
-
1010
- # Parse value: booleans, numbers, strings
1011
- parsed_value: object
1012
- if value.lower() in ("true", "yes", "on"):
1013
- parsed_value = True
1014
- elif value.lower() in ("false", "no", "off"):
1015
- parsed_value = False
1016
- else:
1017
- try:
1018
- parsed_value = int(value)
1019
- except ValueError:
1020
- try:
1021
- parsed_value = float(value)
1022
- except ValueError:
1023
- parsed_value = value
1024
-
1025
- if key == "scope.default_scope" and parsed_value not in {
1026
- "personal", "shared", "global",
1027
- }:
1028
- message = "scope.default_scope must be personal, shared, or global"
1029
- if use_json:
1030
- from superlocalmemory.cli.json_output import json_print
1031
- json_print("config", error={
1032
- "code": "INVALID_VALUE", "message": message,
1033
- })
1034
- else:
1035
- print(f"Error: {message}")
1036
- sys.exit(1)
1037
- if key in {
1038
- "scope.recall_include_global", "scope.recall_include_shared",
1039
- } and not isinstance(parsed_value, bool):
1040
- message = f"{key} must be true or false"
1041
- if use_json:
1042
- from superlocalmemory.cli.json_output import json_print
1043
- json_print("config", error={
1044
- "code": "INVALID_VALUE", "message": message,
1045
- })
1046
- else:
1047
- print(f"Error: {message}")
1048
- sys.exit(1)
1049
-
1050
- _MODEL_KEYS = {
1051
- "evolution.mutation_model", "evolution.verify_model",
1052
- "evolution.confirm_model",
1053
- }
1054
- if key in _MODEL_KEYS:
1055
- from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
1056
-
1057
- accepted = set(_MODEL_ALIASES) | {"", "auto"}
1058
- sval = str(parsed_value)
1059
- if sval not in accepted:
1060
- allowed = ", ".join(["auto", *sorted(_MODEL_ALIASES)])
1061
- message = f"{key} must be one of: {allowed}"
1062
- if use_json:
1063
- from superlocalmemory.cli.json_output import json_print
1064
- json_print("config", error={
1065
- "code": "INVALID_VALUE", "message": message,
1066
- })
1067
- else:
1068
- print(f"Error: {message}")
1069
- sys.exit(1)
1070
- # Normalise the "auto" sentinel to the empty string the
1071
- # resolver treats as "pick the cheapest for the backend".
1072
- if sval == "auto":
1073
- parsed_value = ""
1074
-
1075
- if key.startswith("scope."):
1076
- from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
1077
-
1078
- if is_daemon_running():
1079
- field = key.split(".", 1)[1]
1080
- result = daemon_request(
1081
- "PUT", "/api/v3/scope/config", {field: parsed_value},
1082
- )
1083
- if not isinstance(result, dict) or result.get("success") is not True:
1084
- message = "resident daemon rejected the scope configuration"
1085
- if use_json:
1086
- from superlocalmemory.cli.json_output import json_print
1087
- json_print("config", error={
1088
- "code": "CONFIG_APPLY_FAILED", "message": message,
1089
- })
1090
- else:
1091
- print(f"Error: {message}")
1092
- sys.exit(1)
1093
- old_value = None
1094
- if use_json:
1095
- from superlocalmemory.cli.json_output import json_print
1096
- json_print("config", data={
1097
- "key": key, "old_value": old_value,
1098
- "new_value": result.get(field), "runtime": "daemon",
1099
- })
1100
- else:
1101
- print(f"{key}: applied to resident daemon -> {result.get(field)}")
1102
- return
1103
-
1104
- # Set via dot-notation (e.g. "evolution.enabled" -> cfg["evolution"]["enabled"])
1105
- parts = key.split(".")
1106
- node = cfg
1107
- for part in parts[:-1]:
1108
- if part not in node or not isinstance(node.get(part), dict):
1109
- node[part] = {}
1110
- node = node[part]
1111
- old_value = node.get(parts[-1])
1112
- node[parts[-1]] = parsed_value
1113
-
1114
- # Write back
1115
- config_path.parent.mkdir(parents=True, exist_ok=True)
1116
- config_path.write_text(json.dumps(cfg, indent=2) + "\n")
1117
-
1118
- if use_json:
1119
- from superlocalmemory.cli.json_output import json_print
1120
- json_print("config", data={
1121
- "key": key, "old_value": old_value, "new_value": parsed_value,
1122
- })
1123
- else:
1124
- print(f"{key}: {old_value} -> {parsed_value}")
1125
- if key == "evolution.enabled" and parsed_value is True:
1126
- print(
1127
- "\n⚠ Skill evolution is now ON. It makes background "
1128
- "LLM calls during consolidation\n"
1129
- " (capped at 10 calls/cycle, 3 cycles/day). It defaults "
1130
- "to the lowest-cost model\n"
1131
- " for your backend (Claude → Haiku, Ollama → "
1132
- "local/free).\n"
1133
- " Pick models: slm config set evolution.mutation_model "
1134
- "<auto|haiku|sonnet|ollama>\n"
1135
- " Turn off: slm config set evolution.enabled false"
1136
- )
1137
-
1138
- else:
1139
- if use_json:
1140
- from superlocalmemory.cli.json_output import json_print
1141
- json_print("config", error={
1142
- "code": "UNKNOWN_ACTION",
1143
- "message": f"Unknown action: {action}. Use 'get' or 'set'.",
1144
- })
1145
- else:
1146
- print(f"Unknown config action: {action}. Use 'get' or 'set'.")
1147
- sys.exit(1)
1148
-
1149
-
1150
- def cmd_evolve(args: Namespace) -> None:
1151
- """Run skill evolution for a session (called from Stop hook).
1152
-
1153
- Reads config.json to check if evolution is enabled.
1154
- If enabled, imports SkillEvolver and runs run_post_session().
1155
- If disabled, exits silently (zero output for fire-and-forget).
1156
- """
1157
- import json
1158
-
1159
- from superlocalmemory.infra.data_root import state_path
1160
-
1161
- session_id = getattr(args, "session", "") or ""
1162
- profile = getattr(args, "profile", "default") or "default"
1163
-
1164
- if not session_id:
1165
- return # Silent exit — nothing to do without a session
1166
-
1167
- from superlocalmemory.core.admission import gate_cli_mutation
1168
- from superlocalmemory.core.operation_request import OperationKind
1169
- gate_cli_mutation(OperationKind.EVOLVE_SKILL)
1170
-
1171
- # Check if evolution is enabled via config.json
1172
- config_path = state_path("config.json")
1173
- try:
1174
- cfg = json.loads(config_path.read_text()) if config_path.exists() else {}
1175
- except (json.JSONDecodeError, OSError):
1176
- return # Config unreadable — silent exit
1177
-
1178
- evolution_cfg = cfg.get("evolution", {})
1179
- if not evolution_cfg.get("enabled", False):
1180
- return # Disabled — silent exit
1181
-
1182
- # Heavy imports only if enabled (this runs as a Popen child)
1183
- try:
1184
- from superlocalmemory.evolution.skill_evolver import SkillEvolver
1185
-
1186
- db_path = state_path("memory.db")
1187
- if not db_path.exists():
1188
- return
1189
-
1190
- evolver = SkillEvolver(db_path)
1191
- evolver.run_post_session(session_id, profile)
1192
- except Exception:
1193
- pass # Best-effort — don't crash the Stop hook
1194
-
1195
-
1196
- def cmd_adapters(args: Namespace) -> None:
1197
- """Manage ingestion adapters (Gmail, Calendar, Transcript).
1198
-
1199
- Usage:
1200
- slm adapters list — show all adapters
1201
- slm adapters enable <name> — enable an adapter
1202
- slm adapters disable <name> — disable and stop
1203
- slm adapters start <name> — start running
1204
- slm adapters stop <name> — stop running
1205
- slm adapters status — detailed status
1206
- """
1207
- from superlocalmemory.ingestion.adapter_manager import handle_adapters_cli
1208
- # args.rest contains everything after "adapters"
1209
- rest = getattr(args, 'rest', []) or []
1210
- handle_adapters_cli(rest)
1211
-
1212
-
1213
- # -- Setup & Config (no --json — interactive commands) ---------------------
1214
-
1215
-
1216
- def cmd_setup(args: Namespace) -> None:
1217
- """Run the interactive setup wizard."""
1218
- from superlocalmemory.cli.setup_wizard import run_wizard
1219
-
1220
- run_wizard(auto=getattr(args, "auto", False))
1221
- sys.exit(0) # Force clean exit (background threads from imports may linger)
1222
-
1223
-
1224
- def cmd_mode(args: Namespace) -> None:
1225
- """Get or set the operating mode.
1226
-
1227
- v3.4.43 behavior change: switching modes via this CLI now PRESERVES the
1228
- user's existing embedding, retrieval, evolution, forgetting, and math
1229
- settings. Previously the CLI called ``SLMConfig.for_mode(...)`` which
1230
- re-derived every field from mode defaults — silently clobbering user
1231
- customizations (e.g. a tuned cross-encoder model, a custom embedding
1232
- endpoint, or custom forgetting half-lives). The v3.4.34 ``mode_change=True``
1233
- guard only protected the ``mode`` field itself; everything else was lost.
1234
-
1235
- New rules:
1236
- - Only ``config.mode`` changes.
1237
- - If the user has NO LLM provider configured AND is switching to a mode
1238
- that typically needs one (B or C), mode-appropriate LLM defaults are
1239
- populated to avoid the daemon coming up dead. Existing LLM config
1240
- is preserved as-is.
1241
- - Embedding / retrieval / evolution / forgetting / math: untouched.
1242
- """
1243
- from superlocalmemory.core.config import SLMConfig
1244
- from superlocalmemory.storage.models import Mode
1245
-
1246
- config = SLMConfig.load()
1247
-
1248
- if args.value:
1249
- from superlocalmemory.core.admission import gate_cli_mutation
1250
- from superlocalmemory.core.operation_request import OperationKind
1251
- gate_cli_mutation(OperationKind.MODE_CHANGE)
1252
-
1253
- if getattr(args, 'json', False):
1254
- from superlocalmemory.cli.json_output import json_print
1255
- if args.value:
1256
- old_mode = config.mode.value.upper()
1257
- updated = SLMConfig.switch_mode(args.value)
1258
- json_print("mode", data={
1259
- "previous_mode": old_mode, "current_mode": args.value.upper(),
1260
- }, next_actions=[
1261
- {"command": "slm status --json", "description": "Check system status"},
1262
- ])
1263
- else:
1264
- json_print("mode", data={"current_mode": config.mode.value.upper()},
1265
- next_actions=[
1266
- {"command": "slm mode a --json", "description": "Switch to zero-cloud mode"},
1267
- {"command": "slm mode c --json", "description": "Switch to full-power mode"},
1268
- ])
1269
- return
1270
-
1271
- if args.value:
1272
- updated = SLMConfig.switch_mode(args.value)
1273
- print(f"Mode set to: {args.value.upper()}")
1274
- print(f" Embedding: {updated.embedding.provider}/{updated.embedding.model_name}")
1275
- if args.value.lower() != "a":
1276
- print(f" LLM: {updated.llm.provider}/{updated.llm.model}")
1277
- print(f" Reranker: ONNX cross-encoder (enabled)")
1278
-
1279
- # V3.3.4: Warn if Mode C lacks cloud API key
1280
- if args.value == "c" and not updated.llm.api_key:
1281
- print(" ⚠ Mode C requires a cloud API key. Run: slm provider set")
1282
- print(" ℹ Run `slm restart` to apply the new mode.")
1283
- else:
1284
- print(f"Current mode: {config.mode.value.upper()}")
1285
-
1286
-
1287
- def cmd_provider(args: Namespace) -> None:
1288
- """Get or set the LLM provider."""
1289
- from superlocalmemory.core.config import SLMConfig
1290
-
1291
- config = SLMConfig.load()
1292
-
1293
- if args.action == "set":
1294
- from superlocalmemory.cli.setup_wizard import configure_provider
1295
-
1296
- configure_provider(config, provider_name=getattr(args, "provider", None))
1297
- else:
1298
- print(f"Provider: {config.llm.provider or 'none (Mode A)'}")
1299
- if config.llm.model:
1300
- print(f"Model: {config.llm.model}")
1301
-
1302
-
1303
- def _cmd_context_dispatch(args: Namespace) -> None:
1304
- """V3.4.22 LLD-05: ``slm context prestage``."""
1305
- from superlocalmemory.cli.context_commands import cmd_context
1306
- cmd_context(args)
1307
-
1308
-
1309
- def _agents_md_source_factory():
1310
- """Return a callable that reads the bundled AGENTS.md content, or None on failure.
1311
-
1312
- Source: plugin-src/rules/AGENTS.md (relative to package root).
1313
- Gracefully skips if absent — never fails the MCP write.
1314
- """
1315
- from pathlib import Path
1316
- import sysconfig
1317
-
1318
- # Resolve relative to the package root (src/superlocalmemory/../../)
1319
- _pkg_root = Path(__file__).resolve().parents[3]
1320
- _candidates = (
1321
- _pkg_root / "plugin-src" / "rules" / "AGENTS.md",
1322
- Path(sysconfig.get_path("data")) / "share" / "superlocalmemory"
1323
- / "portable-kit" / "rules" / "AGENTS.md",
1324
- )
1325
-
1326
- def _read() -> str | None:
1327
- for candidate in _candidates:
1328
- if candidate.exists():
1329
- return candidate.read_text(encoding="utf-8")
1330
- logger.warning(
1331
- "AGENTS.md is not bundled in this distribution — skipping AGENTS.md write"
1332
- )
1333
- return None
1334
-
1335
- return _read
1336
-
1337
-
1338
- def cmd_connect(args: Namespace) -> None:
1339
- """Configure IDE integrations.
1340
-
1341
- Dispatch priority:
1342
- 1. ``slm connect <ide>`` where ide ∈ IDE_MATRIX → portable_kit.connect_ide
1343
- (MCP-wiring + AGENTS.md; claude-code short-circuits to the plugin pointer).
1344
- 2. ``--cross-platform`` / ``--disable`` → LLD-05 CrossPlatformConnector.
1345
- 3. Bare ``slm connect`` / ``--list`` → legacy IDEConnector (markdown-rules).
1346
- """
1347
- ide_arg = getattr(args, "ide", None)
1348
-
1349
- # Intercept known IDE_MATRIX ids before legacy branches (critical: must run first)
1350
- if ide_arg is not None:
1351
- from superlocalmemory.hooks.portable_kit import (
1352
- IDE_MATRIX,
1353
- connect_ide,
1354
- supported_ides,
1355
- )
1356
-
1357
- if ide_arg in IDE_MATRIX:
1358
- here = getattr(args, "here", False)
1359
- profile = getattr(args, "profile", None)
1360
- transport = getattr(args, "transport", "stdio")
1361
- daemon_port = getattr(args, "daemon_port", 8765)
1362
- verify = getattr(args, "verify", False)
1363
- project = None
1364
- if here:
1365
- import pathlib
1366
- project = pathlib.Path.cwd()
1367
-
1368
- result = connect_ide(
1369
- ide_arg,
1370
- home=None,
1371
- project=project,
1372
- here=here,
1373
- profile=profile,
1374
- agents_md_source=_agents_md_source_factory(),
1375
- dry_run=getattr(args, "dry_run", False),
1376
- transport=transport,
1377
- daemon_port=daemon_port,
1378
- )
1379
-
1380
- # --verify: probe daemon health after writing
1381
- if verify and transport in ("http", "http-mcp-remote") and not result.get("error"):
1382
- from superlocalmemory.hooks.portable_kit import _check_daemon_health
1383
- reachable = _check_daemon_health(daemon_port)
1384
- if reachable:
1385
- print(f"[verify] Daemon reachable at http://127.0.0.1:{daemon_port}/api/v3/health")
1386
- else:
1387
- print(
1388
- f"[verify] Warning: daemon NOT reachable at "
1389
- f"http://127.0.0.1:{daemon_port}/api/v3/health. "
1390
- f"Run `slm serve start` to start it.",
1391
- file=sys.stderr,
1392
- )
1393
-
1394
- if not result.get("error") and not getattr(args, "dry_run", False):
1395
- from superlocalmemory.infra.local_diagnostics import record_operation
1396
-
1397
- record_operation("activation", client=ide_arg)
1398
-
1399
- if getattr(args, "json", False):
1400
- from superlocalmemory.cli.json_output import json_print
1401
- json_print("connect", data=result)
1402
- return
1403
-
1404
- if result["error"]:
1405
- print(f"Error: {result['error']}", file=sys.stderr)
1406
- print(
1407
- f"Supported IDEs: {', '.join(supported_ides())}",
1408
- file=sys.stderr,
1409
- )
1410
- sys.exit(1)
1411
-
1412
- status_sym = {"wrote": "[+]", "merged": "[~]", "unchanged": "[=]",
1413
- "would_write": "[~]",
1414
- "skipped": "[s]", "error": "[!]"}.get(
1415
- result["mcp_config"], "[?]"
1416
- )
1417
- print(
1418
- f"{status_sym} {ide_arg}: mcp_config={result['mcp_config']} "
1419
- f"path={result['mcp_path']}"
1420
- )
1421
- print(f" agents_md={result['agents_md']}")
1422
- return
1423
-
1424
- # Unknown ide — list supported and exit non-zero
1425
- from superlocalmemory.hooks.portable_kit import supported_ides
1426
- print(
1427
- f"Unknown IDE '{ide_arg}'.\nSupported: {', '.join(supported_ides())}",
1428
- file=sys.stderr,
1429
- )
1430
- sys.exit(1)
1431
-
1432
- # Route --disable <name> and --cross-platform to the LLD-05 orchestrator.
1433
- if getattr(args, "disable", None) or getattr(args, "cross_platform", False):
1434
- from superlocalmemory.cli.context_commands import (
1435
- cmd_connect_cross_platform,
1436
- )
1437
- cmd_connect_cross_platform(args)
1438
- return
1439
- from superlocalmemory.hooks.ide_connector import IDEConnector
1440
-
1441
- connector = IDEConnector()
1442
- dry_run = bool(getattr(args, "dry_run", False))
1443
-
1444
- if getattr(args, 'json', False):
1445
- from superlocalmemory.cli.json_output import json_print
1446
- if getattr(args, "list", False):
1447
- json_print("connect", data={"ides": connector.get_status()},
1448
- next_actions=[
1449
- {"command": "slm connect --json", "description": "Auto-configure all IDEs"},
1450
- ])
1451
- elif getattr(args, "ide", None):
1452
- success = connector.connect(args.ide)
1453
- json_print("connect", data={"ide": args.ide, "connected": success})
1454
- elif dry_run:
1455
- json_print(
1456
- "connect",
1457
- data={
1458
- "dry_run": True,
1459
- "would_configure": connector.get_status(),
1460
- },
1461
- )
1462
- else:
1463
- json_print("connect", data={"results": connector.connect_all()},
1464
- next_actions=[
1465
- {"command": "slm status --json", "description": "Check system status"},
1466
- ])
1467
- return
1468
-
1469
- if getattr(args, "list", False):
1470
- status = connector.get_status()
1471
- for s in status:
1472
- mark = "[+]" if s["installed"] else "[-]"
1473
- print(f" {mark} {s['name']:20s} {s['config_path']}")
1474
- return
1475
- if dry_run:
1476
- status = connector.get_status()
1477
- print("Dry run — no IDE, hook, config, or SLM data files were written.")
1478
- for item in status:
1479
- print(f" would inspect/configure: {item['name']} ({item['config_path']})")
1480
- return
1481
- if getattr(args, "ide", None):
1482
- success = connector.connect(args.ide)
1483
- print(f"{'Connected' if success else 'Failed'}: {args.ide}")
1484
- else:
1485
- results = connector.connect_all()
1486
- for ide_id, ide_status in results.items():
1487
- print(f" {ide_id}: {ide_status}")
1488
-
1489
-
1490
- def cmd_migrate(args: Namespace) -> None:
1491
- """Run V2 to V3 migration."""
1492
- from superlocalmemory.cli.migrate_cmd import cmd_migrate as _migrate
1493
-
1494
- _migrate(args)
1495
-
1496
-
1497
- # -- Memory Operations (all support --json) --------------------------------
1498
-
1499
-
1500
- def cmd_list(args: Namespace) -> None:
1501
- """List recent memories chronologically."""
1502
- from superlocalmemory.core.config import CANONICAL_LIST_LIMIT, SLMConfig
1503
- from superlocalmemory.core.engine import MemoryEngine
1504
-
1505
- use_json = getattr(args, 'json', False)
1506
- try:
1507
- config = SLMConfig.load()
1508
- engine = MemoryEngine(config)
1509
- engine.initialize()
1510
-
1511
- limit = getattr(args, "limit", CANONICAL_LIST_LIMIT)
1512
- # The query already returns newest-first; pushing the bound into SQL
1513
- # keeps this from deserializing the whole table to show twenty rows.
1514
- facts = engine._db.get_all_facts(engine.profile_id, limit=limit)
1515
- except Exception as exc:
1516
- if use_json:
1517
- from superlocalmemory.cli.json_output import json_print
1518
- json_print("list", error={"code": "ENGINE_ERROR", "message": str(exc)})
1519
- sys.exit(1)
1520
- raise
1521
-
1522
- if use_json:
1523
- from superlocalmemory.cli.json_output import json_print
1524
- items = []
1525
- for f in facts:
1526
- ftype_raw = getattr(f, "fact_type", "")
1527
- ftype = ftype_raw.value if hasattr(ftype_raw, "value") else str(ftype_raw)
1528
- items.append({
1529
- "fact_id": f.fact_id, "content": f.content,
1530
- "fact_type": ftype, "created_at": (f.created_at or "")[:19],
1531
- })
1532
- json_print("list", data={"results": items, "count": len(items)},
1533
- next_actions=[
1534
- {"command": "slm recall '<query>' --json", "description": "Search memories"},
1535
- {"command": "slm delete <fact_id> --json --yes", "description": "Delete a memory"},
1536
- ])
1537
- return
1538
-
1539
- if not facts:
1540
- print("No memories stored yet.")
1541
- else:
1542
- print(f"Recent memories ({len(facts)}):\n")
1543
- for i, f in enumerate(facts, 1):
1544
- date = (f.created_at or "")[:19]
1545
- ftype_raw = getattr(f, "fact_type", "")
1546
- ftype = ftype_raw.value if hasattr(ftype_raw, "value") else str(ftype_raw)
1547
- content = f.content[:100] + ("..." if len(f.content) > 100 else "")
1548
- print(f" {i:3d}. [{date}] ({ftype}) {content}")
1549
-
1550
- # V3.3.21: Show pending memories (store-first pattern)
1551
- try:
1552
- from superlocalmemory.cli.pending_store import get_pending
1553
- pending = get_pending(limit=10)
1554
- if pending:
1555
- print(f"\nPending (processing in background): {len(pending)}")
1556
- for p in pending:
1557
- content = p["content"][:80] + ("..." if len(p["content"]) > 80 else "")
1558
- print(f" \u23f3 [{p['created_at'][:19]}] {content}")
1559
- except Exception:
1560
- pass
1561
-
1562
-
1563
- def cmd_remember(args: Namespace) -> None:
1564
- """Store a memory through the owned daemon."""
1565
-
1566
- use_json = getattr(args, 'json', False)
1567
- sync_mode = getattr(args, 'sync_mode', False)
1568
- # v3.6.15 multi-scope: scope=None means "not specified" → resolve to the
1569
- # configured default_scope (personal) at the daemon / engine boundary.
1570
- # Shared memory is opt-in, so an unset --scope always stays private.
1571
- scope = getattr(args, 'scope', None)
1572
- # v3.6.15: --shared-with is a comma-separated string on the CLI, but the
1573
- # daemon (RememberRequest) and engine expect list[str]. Parse here so an
1574
- # explicit `--shared-with a,b` doesn't 422 at the daemon and silently fall
1575
- # back to a personal write.
1576
- _sw_raw = getattr(args, 'shared_with', None)
1577
- shared_with = (
1578
- [s.strip() for s in _sw_raw.split(",") if s.strip()]
1579
- if isinstance(_sw_raw, str) and _sw_raw.strip() else _sw_raw
1580
- )
1581
-
1582
- try:
1583
- from superlocalmemory.cli.daemon import (
1584
- daemon_request, ensure_daemon, is_daemon_running,
1585
- )
1586
- if not (is_daemon_running() or ensure_daemon()):
1587
- _daemon_unavailable("remember", use_json)
1588
- path = "/remember?wait=true" if sync_mode else "/remember"
1589
- result = daemon_request(
1590
- "POST", path, {
1591
- "content": args.content,
1592
- "tags": args.tags or "",
1593
- "scope": scope,
1594
- "shared_with": shared_with,
1595
- },
1596
- timeout_seconds=30,
1597
- )
1598
- if result and "fact_ids" in result:
1599
- if use_json:
1600
- from superlocalmemory.cli.json_output import json_print
1601
- json_print("remember", data=result)
1602
- else:
1603
- state = result.get("materialization_state", "queryable")
1604
- operation_id = result.get("operation_id", "unknown")
1605
- print(
1606
- f"{state.capitalize()} \u2713 {result['count']} facts "
1607
- f"(operation={operation_id})."
1608
- )
1609
- return
1610
- except SystemExit:
1611
- raise
1612
- except Exception as exc:
1613
- logger.warning("owned daemon remember request failed: %s", exc)
1614
- _daemon_unavailable("remember", use_json)
1615
-
1616
-
1617
- def cmd_recall(args: Namespace) -> None:
1618
- """Search memories through the owned daemon without a local engine."""
1619
- use_json = getattr(args, 'json', False)
1620
- # v3.6.15: None = "not specified" → daemon/engine resolves the configured
1621
- # default (shared-off). Only an explicit --include-global / --no-global
1622
- # produces True/False here.
1623
- include_global = getattr(args, 'include_global', None)
1624
- include_shared = getattr(args, 'include_shared', None)
1625
- # Phase-1/D2: clamp cross-profile scope flags before forwarding to daemon.
1626
- from superlocalmemory.core.admission import enforce_read_scope
1627
- include_global, include_shared = enforce_read_scope(include_global, include_shared)
1628
-
1629
- # V3.3.21: Route through daemon for instant response (no cold start).
1630
- # S9-DASH-02: pass a stable session_id derived from the shell's
1631
- # parent PID so a sequence of CLI recalls in one terminal can be
1632
- # grouped. The Stop hook on session end won't fire for CLI, so
1633
- # these outcomes are closed by the reaper (TTL → neutral reward).
1634
- try:
1635
- from superlocalmemory.cli.daemon import is_daemon_running, daemon_request, ensure_daemon
1636
- if is_daemon_running() or ensure_daemon():
1637
- from urllib.parse import quote
1638
- session_id = f"cli:{os.getppid()}"
1639
- fast_qs = "&fast=true" if getattr(args, "fast", False) else ""
1640
- # Only send scope flags when the user set them explicitly; absent =
1641
- # let the daemon resolve the configured default. (Never emit
1642
- # "none" — that would parse as a missing/false value.)
1643
- scope_qs = ""
1644
- if include_global is not None:
1645
- scope_qs += f"&include_global={str(include_global).lower()}"
1646
- if include_shared is not None:
1647
- scope_qs += f"&include_shared={str(include_shared).lower()}"
1648
- _window = getattr(args, "window", "") or ""
1649
- window_qs = f"&window={quote(_window)}" if _window else ""
1650
- _as_of = getattr(args, "as_of", "") or ""
1651
- if _as_of:
1652
- from superlocalmemory.retrieval.temporal_utils import normalize_as_of
1653
- _as_of_norm = normalize_as_of(_as_of)
1654
- if _as_of_norm is None:
1655
- import sys as _sys
1656
- _sys.stderr.write(
1657
- f"Error: invalid --as-of value: {_as_of!r}\n"
1658
- "Expected ISO 8601 UTC datetime, e.g. '2024-01-01T00:00:00Z'.\n"
1659
- )
1660
- _sys.exit(1)
1661
- _as_of = _as_of_norm
1662
- as_of_qs = f"&as_of={quote(_as_of)}" if _as_of else ""
1663
- def _strict_time_qs(attr: str, parameter: str) -> str:
1664
- raw = getattr(args, attr, "") or ""
1665
- if not raw:
1666
- return ""
1667
- from superlocalmemory.retrieval.temporal_utils import normalize_as_of
1668
- normalized = normalize_as_of(raw)
1669
- if normalized is None:
1670
- import sys as _sys
1671
- _sys.stderr.write(
1672
- f"Error: invalid --{attr.replace('_', '-')} value: {raw!r}\n"
1673
- )
1674
- _sys.exit(1)
1675
- return f"&{parameter}={quote(normalized)}"
1676
- known_as_of_qs = _strict_time_qs("known_as_of", "known_as_of")
1677
- valid_at_qs = _strict_time_qs("valid_at", "valid_at")
1678
- unknown_qs = "&include_unknown=true" if getattr(args, "include_unknown", False) else ""
1679
- result = daemon_request(
1680
- "GET",
1681
- f"/recall?q={quote(args.query)}&limit={args.limit}"
1682
- f"&session_id={quote(session_id)}{fast_qs}{scope_qs}{window_qs}{as_of_qs}"
1683
- f"{known_as_of_qs}{valid_at_qs}{unknown_qs}",
1684
- )
1685
- if result and "results" in result:
1686
- # Format daemon response same as engine response
1687
- if use_json:
1688
- from superlocalmemory.cli.json_output import json_print
1689
- json_print("recall", data=result, next_actions=[
1690
- {"command": "slm list --json", "description": "List recent memories"},
1691
- ])
1692
- return
1693
- if not result["results"]:
1694
- print("No confident match."
1695
- if result.get("no_confident_match")
1696
- else "No matching memories found.")
1697
- return
1698
- # Text output.
1699
- # PR #101: ``dict.get(k, 0)`` returns the DEFAULT only when the
1700
- # key is ABSENT — a present-but-null value still reaches the
1701
- # format spec and raises "unsupported format string passed to
1702
- # NoneType.__format__". ``or 0`` covers both. Same for score,
1703
- # which the keyword-fallback path returns as None.
1704
- elapsed_ms = result.get('retrieval_time_ms') or 0
1705
- print(
1706
- "SpreadingActivation.search completed via daemon "
1707
- f"({elapsed_ms:.0f}ms)"
1708
- )
1709
- for i, r in enumerate(result["results"], 1):
1710
- score = r.get('score') or 0
1711
- print(f" {i}. [{score:.2f}] {r['content']}")
1712
- return
1713
- except Exception as _exc: # noqa: BLE001
1714
- logger.warning(
1715
- "owned daemon recall request failed: %s", _exc
1716
- )
1717
-
1718
- _daemon_unavailable("recall", use_json)
1719
-
1720
-
1721
- def _cli_record_signals(config, query, results):
1722
- """Record learning signals from CLI recall (no MCP dependency)."""
1723
- from pathlib import Path
1724
-
1725
- from superlocalmemory.learning.feedback import FeedbackCollector
1726
- from superlocalmemory.learning.signals import LearningSignals
1727
- configured_root = getattr(config, "base_dir", None)
1728
- if configured_root is not None:
1729
- slm_dir = Path(configured_root)
1730
- else:
1731
- from superlocalmemory.infra.data_root import canonical_data_root
1732
- slm_dir = canonical_data_root()
1733
- pid = config.active_profile
1734
- fact_ids = [r.fact.fact_id for r in results[:10]]
1735
- if not fact_ids:
1736
- return
1737
- FeedbackCollector(slm_dir / "learning.db").record_implicit(
1738
- profile_id=pid, query=query,
1739
- fact_ids_returned=fact_ids, fact_ids_available=fact_ids,
1740
- )
1741
- signals = LearningSignals(slm_dir / "learning.db")
1742
- signals.record_co_retrieval(pid, fact_ids)
1743
- for fid in fact_ids[:5]:
1744
- LearningSignals.boost_confidence(str(slm_dir / "memory.db"), fid)
1745
-
1746
-
1747
- def cmd_forget(args: Namespace) -> None:
1748
- """Delete daemon-queried memories matching a query."""
1749
- from superlocalmemory.core.admission import gate_cli_mutation
1750
- from superlocalmemory.core.operation_request import OperationKind
1751
- gate_cli_mutation(OperationKind.FORGET)
1752
-
1753
- import urllib.parse
1754
-
1755
- from superlocalmemory.cli.daemon import (
1756
- daemon_request,
1757
- ensure_daemon,
1758
- is_daemon_running,
1759
- )
1760
-
1761
- use_json = getattr(args, 'json', False)
1762
- dry_run = getattr(args, 'dry_run', False)
1763
- raw_query = getattr(args, 'query', None)
1764
-
1765
- # F3: `query` is optional so `slm forget --dry-run` can preview every memory.
1766
- # Deletion ALWAYS requires an explicit query — a bare `slm forget` must never
1767
- # mass-delete. In dry-run mode a missing query means "match all" (preview).
1768
- if raw_query is None and not dry_run:
1769
- msg = (
1770
- "A query is required to delete. Preview everything with "
1771
- "'slm forget --dry-run', or delete matches with 'slm forget <query>'."
1772
- )
1773
- if use_json:
1774
- from superlocalmemory.cli.json_output import json_print
1775
- json_print("forget", error={"code": "QUERY_REQUIRED", "message": msg})
1776
- else:
1777
- print(msg)
1778
- sys.exit(2)
1779
-
1780
- query_lower = "" if raw_query is None else raw_query.lower()
1781
- query_label = raw_query if raw_query is not None else "*all*"
1782
-
1783
- if not (is_daemon_running() or ensure_daemon()):
1784
- _daemon_unavailable("forget", use_json)
1785
-
1786
- memories: list[dict[str, object]] = []
1787
- offset = 0
1788
- while True:
1789
- page = daemon_request("GET", f"/api/memories?limit=200&offset={offset}")
1790
- rows = page.get("memories") if isinstance(page, dict) else None
1791
- if not isinstance(rows, list):
1792
- _daemon_unavailable("forget", use_json)
1793
- memories.extend(row for row in rows if isinstance(row, dict))
1794
- if not page.get("has_more"):
1795
- break
1796
- offset += len(rows)
1797
- if not rows:
1798
- _daemon_unavailable("forget", use_json)
1799
-
1800
- matches = [
1801
- fact for fact in memories
1802
- if query_lower in str(fact.get("content", "")).lower()
1803
- ]
1804
-
1805
- def delete_from_daemon(fact_id: str) -> None:
1806
- result = daemon_request(
1807
- "DELETE",
1808
- "/api/memories/" + urllib.parse.quote(fact_id, safe=""),
1809
- )
1810
- if not isinstance(result, dict) or not result.get("success"):
1811
- _daemon_unavailable("forget", use_json)
1812
-
1813
- if use_json:
1814
- from superlocalmemory.cli.json_output import json_print
1815
- if not matches:
1816
- json_print("forget", data={"matched_count": 0, "deleted_count": 0, "matches": []})
1817
- return
1818
- match_items = [
1819
- {"fact_id": f["id"], "content": str(f.get("content", ""))[:120]}
1820
- for f in matches[:20]
1821
- ]
1822
- if dry_run:
1823
- json_print("forget", data={
1824
- "matched_count": len(matches), "deleted_count": 0,
1825
- "dry_run": True, "matches": match_items,
1826
- })
1827
- return
1828
- if getattr(args, 'yes', False):
1829
- for f in matches:
1830
- delete_from_daemon(str(f["id"]))
1831
- json_print("forget", data={
1832
- "matched_count": len(matches), "deleted_count": len(matches),
1833
- "deleted": [f["id"] for f in matches],
1834
- }, next_actions=[
1835
- {"command": "slm list --json", "description": "Verify remaining memories"},
1836
- ])
1837
- else:
1838
- json_print("forget", data={
1839
- "matched_count": len(matches), "deleted_count": 0,
1840
- "matches": match_items,
1841
- "hint": "Add --yes to confirm deletion",
1842
- }, next_actions=[
1843
- {
1844
- "command": f"slm forget '{query_label}' --json --yes",
1845
- "description": "Confirm deletion",
1846
- },
1847
- ])
1848
- return
1849
-
1850
- if not matches:
1851
- print(f"No memories matching '{query_label}'")
1852
- return
1853
- print(f"Found {len(matches)} matching memories:")
1854
- for f in matches[:10]:
1855
- print(f" - {str(f['id'])[:8]}... {str(f.get('content', ''))[:80]}")
1856
- if dry_run:
1857
- print(f"(dry run — {len(matches)} would be deleted)")
1858
- return
1859
- if getattr(args, 'yes', False):
1860
- for f in matches:
1861
- delete_from_daemon(str(f["id"]))
1862
- print(f"Deleted {len(matches)} memories.")
1863
- return
1864
- confirm = input(f"Delete {len(matches)} memories? [y/N] ").strip().lower()
1865
- if confirm in ("y", "yes"):
1866
- for f in matches:
1867
- delete_from_daemon(str(f["id"]))
1868
- print(f"Deleted {len(matches)} memories.")
1869
- else:
1870
- print("Cancelled.")
1871
-
1872
-
1873
- def cmd_delete(args: Namespace) -> None:
1874
- """Delete a specific memory by exact fact ID."""
1875
- from superlocalmemory.core.admission import gate_cli_mutation
1876
- from superlocalmemory.core.operation_request import OperationKind
1877
- gate_cli_mutation(OperationKind.FORGET)
1878
-
1879
- import urllib.parse
1880
-
1881
- from superlocalmemory.cli.daemon import (
1882
- daemon_request,
1883
- ensure_daemon,
1884
- is_daemon_running,
1885
- )
1886
-
1887
- use_json = getattr(args, 'json', False)
1888
- fact_id = args.fact_id.strip()
1889
- if is_daemon_running() or ensure_daemon():
1890
- path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
1891
- confirmed = getattr(args, "yes", False)
1892
- content = ""
1893
- if not confirmed:
1894
- detail = daemon_request(
1895
- "GET",
1896
- "/api/facts/" + urllib.parse.quote(fact_id, safe=""),
1897
- )
1898
- if not isinstance(detail, dict):
1899
- _daemon_unavailable("delete", use_json)
1900
- content = str(detail.get("content") or "")
1901
- if use_json:
1902
- from superlocalmemory.cli.json_output import json_print
1903
- json_print("delete", data={
1904
- "fact_id": fact_id,
1905
- "content": content[:120],
1906
- "deleted": False,
1907
- "hint": "Add --yes to confirm deletion",
1908
- }, next_actions=[
1909
- {
1910
- "command": f"slm delete {fact_id} --json --yes",
1911
- "description": "Confirm deletion",
1912
- },
1913
- ])
1914
- return
1915
- print(f"Memory: {content[:120]}")
1916
- confirmed = input("Delete this memory? [y/N] ").strip().lower() in (
1917
- "y", "yes",
1918
- )
1919
- if not confirmed:
1920
- print("Cancelled.")
1921
- return
1922
-
1923
- result = daemon_request("DELETE", path)
1924
- if not isinstance(result, dict) or not result.get("success"):
1925
- _daemon_unavailable("delete", use_json)
1926
- if use_json:
1927
- from superlocalmemory.cli.json_output import json_print
1928
- json_print(
1929
- "delete",
1930
- data={"deleted": fact_id, "content": content[:120]},
1931
- next_actions=[
1932
- {
1933
- "command": "slm list --json",
1934
- "description": "Verify remaining memories",
1935
- },
1936
- ],
1937
- )
1938
- else:
1939
- print(f"Deleted: {fact_id}")
1940
- return
1941
- _daemon_unavailable("delete", use_json)
1942
-
1943
-
1944
- def cmd_update(args: Namespace) -> None:
1945
- """Update the content of a specific memory by exact fact ID."""
1946
- from superlocalmemory.core.admission import gate_cli_mutation
1947
- from superlocalmemory.core.operation_request import OperationKind
1948
- gate_cli_mutation(OperationKind.CORRECT)
1949
-
1950
- import urllib.parse
1951
-
1952
- from superlocalmemory.cli.daemon import (
1953
- daemon_request,
1954
- ensure_daemon,
1955
- is_daemon_running,
1956
- )
1957
-
1958
- use_json = getattr(args, 'json', False)
1959
- fact_id = args.fact_id.strip()
1960
- new_content = args.content.strip()
1961
-
1962
- if not new_content:
1963
- if use_json:
1964
- from superlocalmemory.cli.json_output import json_print
1965
- json_print("update", error={"code": "INVALID_INPUT", "message": "content cannot be empty"})
1966
- sys.exit(1)
1967
- print("Error: content cannot be empty")
1968
- return
1969
-
1970
- if is_daemon_running() or ensure_daemon():
1971
- path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
1972
- result = daemon_request("PATCH", path, {"content": new_content})
1973
- if not isinstance(result, dict) or not result.get("success"):
1974
- _daemon_unavailable("update", use_json)
1975
- if use_json:
1976
- from superlocalmemory.cli.json_output import json_print
1977
- json_print("update", data={
1978
- "predecessor_fact_id": result.get("predecessor_fact_id", fact_id),
1979
- "successor_fact_id": result.get("successor_fact_id"),
1980
- "correction_case": result.get("correction_case"),
1981
- "review_required": bool(result.get("review_required", False)),
1982
- }, next_actions=[
1983
- {
1984
- "command": "slm brain --json",
1985
- "description": "Inspect the observation-only brain status",
1986
- },
1987
- ])
1988
- else:
1989
- if result.get("review_required"):
1990
- case = result.get("correction_case", {})
1991
- print(f"Correction proposed: {case.get('case_id', 'unknown')}")
1992
- print(f"Predecessor remains current until review: {fact_id}")
1993
- else:
1994
- print(f"Unchanged: {fact_id}")
1995
- return
1996
- _daemon_unavailable("update", use_json)
1997
-
1998
-
1999
- def cmd_review_correction(args: Namespace) -> None:
2000
- """Apply, reject, or roll back an explicitly reviewed correction case."""
2001
- from superlocalmemory.core.admission import gate_cli_mutation
2002
- from superlocalmemory.core.operation_request import OperationKind
2003
-
2004
- gate_cli_mutation(OperationKind.CORRECT)
2005
- import urllib.parse
2006
-
2007
- from superlocalmemory.cli.daemon import daemon_request, ensure_daemon, is_daemon_running
2008
-
2009
- use_json = getattr(args, "json", False)
2010
- action = str(args.action).strip().lower()
2011
- if action not in {"apply", "reject", "rollback"}:
2012
- raise ValueError("action must be apply, reject, or rollback")
2013
- if not isinstance(args.expected_version, int) or args.expected_version < 0:
2014
- raise ValueError("expected_version must be a non-negative integer")
2015
- if not (is_daemon_running() or ensure_daemon()):
2016
- _daemon_unavailable("correction review", use_json)
2017
- return
2018
- payload: dict[str, object] = {"expected_version": args.expected_version}
2019
- if getattr(args, "event_valid_until", None):
2020
- payload["event_valid_until"] = args.event_valid_until
2021
- path = "/api/corrections/" + urllib.parse.quote(args.case_id, safe="") + "/" + action
2022
- result = daemon_request("POST", path, payload)
2023
- if not isinstance(result, dict) or not result.get("success"):
2024
- _daemon_unavailable("correction review", use_json)
2025
- return
2026
- if use_json:
2027
- from superlocalmemory.cli.json_output import json_print
2028
-
2029
- json_print("review-correction", data=result)
2030
- else:
2031
- case = result.get("correction_case", {})
2032
- print(f"Correction {action}: {case.get('case_id', args.case_id)}")
2033
- print(f"State: {case.get('status', 'unknown')}")
2034
-
2035
-
2036
- # -- Diagnostics (all support --json) -------------------------------------
2037
-
2038
-
2039
- def cmd_status(args: Namespace) -> None:
2040
- """Show system status."""
2041
- from superlocalmemory.core.config import SLMConfig
2042
- # Same source the other two status surfaces read, so all three cannot
2043
- # disagree about which version answered.
2044
- from superlocalmemory.server.routes.helpers import SLM_VERSION
2045
-
2046
- config = SLMConfig.load()
2047
- daemon_status = None
2048
- try:
2049
- from superlocalmemory.cli.daemon import (
2050
- daemon_request,
2051
- is_daemon_running,
2052
- )
2053
-
2054
- if is_daemon_running():
2055
- candidate = daemon_request("GET", "/status")
2056
- if isinstance(candidate, dict) and candidate.get("profile"):
2057
- daemon_status = candidate
2058
- except Exception:
2059
- logger.debug(
2060
- "cmd_status: daemon runtime status unavailable; using offline view",
2061
- exc_info=True,
2062
- )
2063
-
2064
- if getattr(args, 'json', False):
2065
- from superlocalmemory.cli.json_output import json_print
2066
-
2067
- if daemon_status is not None:
2068
- data = {
2069
- # Lowercase is what the daemon, the HTTP surface and MCP all
2070
- # report. Uppercasing it here made one field disagree across
2071
- # surfaces for anyone comparing the two answers.
2072
- "mode": str(daemon_status.get("mode", "unknown")),
2073
- "provider": daemon_status.get("provider", "none"),
2074
- "profile": daemon_status["profile"],
2075
- "base_dir": daemon_status.get("base_dir", str(config.base_dir)),
2076
- "db_path": daemon_status.get("db_path", str(config.db_path)),
2077
- "db_size_mb": float(daemon_status.get("db_size_mb", 0.0)),
2078
- "fact_count": int(daemon_status.get("fact_count", 0)),
2079
- "entity_count": int(daemon_status.get("entity_count", 0)),
2080
- "edge_count": int(daemon_status.get("edge_count", 0)),
2081
- "profile_generation": int(
2082
- daemon_status.get("profile_generation", 0)
2083
- ),
2084
- "version": SLM_VERSION,
2085
- "projection_queue_depth": int(
2086
- daemon_status.get("projection_queue_depth", 0)
2087
- ),
2088
- }
2089
- json_print("status", data=data, next_actions=[
2090
- {"command": "slm health --json", "description": "Check math layer health"},
2091
- {"command": "slm list --json", "description": "List recent memories"},
2092
- ])
2093
- return
2094
-
2095
- # canonical key set — db_size_mb always present (0.0 if absent).
2096
- db_size_mb = 0.0
2097
- if config.db_path.exists():
2098
- db_size_mb = round(config.db_path.stat().st_size / 1024 / 1024, 2)
2099
-
2100
- # Open engine for counts (json branch only — LLD Decision B).
2101
- # Fail-open to 0 on any error; status must never crash.
2102
- # Guard on db existence: `slm status --json` must stay observational —
2103
- # opening the engine on a fresh install would create + migrate the db
2104
- # (MemoryEngine.initialize → DatabaseManager mkdir/connect/DDL). A
2105
- # previously read-only command must not acquire a write side-effect.
2106
- fact_count = 0
2107
- entity_count = 0
2108
- edge_count = 0
2109
- eng = None
2110
- if config.db_path.exists():
2111
- try:
2112
- from superlocalmemory.core.engine import MemoryEngine
2113
- from superlocalmemory.core.engine_capabilities import Capabilities
2114
- eng = MemoryEngine(config, capabilities=Capabilities.LIGHT)
2115
- eng.initialize()
2116
- pid = config.active_profile
2117
- fact_count = eng._db.get_fact_count(pid)
2118
- rows = eng._db.execute(
2119
- "SELECT COUNT(*) AS c FROM canonical_entities WHERE profile_id = ?",
2120
- (pid,),
2121
- )
2122
- entity_count = int(dict(rows[0])["c"]) if rows else 0
2123
- rows2 = eng._db.execute(
2124
- "SELECT COUNT(*) AS c FROM graph_edges WHERE profile_id = ?",
2125
- (pid,),
2126
- )
2127
- edge_count = int(dict(rows2[0])["c"]) if rows2 else 0
2128
- except Exception:
2129
- logger.debug("cmd_status: engine count query failed; using 0s", exc_info=True)
2130
- finally:
2131
- if eng is not None:
2132
- try:
2133
- eng.close()
2134
- except Exception:
2135
- pass
2136
-
2137
- data = {
2138
- "mode": config.mode.value,
2139
- "provider": config.llm.provider or "none",
2140
- "profile": config.active_profile,
2141
- "base_dir": str(config.base_dir),
2142
- "db_path": str(config.db_path),
2143
- "db_size_mb": db_size_mb,
2144
- "fact_count": fact_count,
2145
- "entity_count": entity_count,
2146
- "edge_count": edge_count,
2147
- "profile_generation": 0,
2148
- "version": SLM_VERSION,
2149
- "projection_queue_depth": _cli_projection_queue_depth(config.db_path),
2150
- }
2151
- json_print("status", data=data, next_actions=[
2152
- {"command": "slm health --json", "description": "Check math layer health"},
2153
- {"command": "slm list --json", "description": "List recent memories"},
2154
- ])
2155
- return
2156
-
2157
- print("SuperLocalMemory V4")
2158
- print(f" Mode: {config.mode.value.upper()}")
2159
- print(f" Provider: {config.llm.provider or 'none'}")
2160
- print(
2161
- f" Profile: "
2162
- f"{daemon_status.get('profile') if daemon_status else config.active_profile}"
2163
- )
2164
- print(f" Base dir: {config.base_dir}")
2165
- print(f" Database: {config.db_path}")
2166
- if config.db_path.exists():
2167
- size_mb = round(config.db_path.stat().st_size / 1024 / 1024, 2)
2168
- print(f" DB size: {size_mb} MB")
2169
-
2170
- # S9-UX-07 / S9-UX-13: --verbose surfaces the disabled marker,
2171
- # last-version marker, and daemon port so users who are debugging
2172
- # "slm seems broken" get the signal without opening three files.
2173
- if getattr(args, "verbose", False):
2174
- home = config.base_dir
2175
- disabled_path = home / ".disabled"
2176
- version_path = home / ".last_version"
2177
- print()
2178
- print(" --verbose --")
2179
- print(
2180
- f" Disabled marker: "
2181
- f"{'YES (slm enable to reactivate)' if disabled_path.exists() else 'no'}",
2182
- )
2183
- if version_path.exists():
2184
- try:
2185
- last_ver = version_path.read_text(encoding="utf-8").strip()
2186
- except OSError:
2187
- last_ver = "(unreadable)"
2188
- print(f" Last booted version: {last_ver}")
2189
- else:
2190
- print(" Last booted version: (never booted)")
2191
- port = os.environ.get("SLM_DAEMON_PORT") or "8765"
2192
- print(f" Daemon port: {port}")
2193
-
2194
-
2195
- def cmd_health(args: Namespace) -> None:
2196
- """Show math layer health status."""
2197
- from superlocalmemory.core.config import SLMConfig
2198
-
2199
- use_json = getattr(args, 'json', False)
2200
- try:
2201
- config = SLMConfig.load()
2202
- from superlocalmemory.cli.daemon import is_daemon_running
2203
- if is_daemon_running():
2204
- # A running daemon owns the writable SQLite connections. Opening a
2205
- # second MemoryEngine re-runs schema initialization and can lock
2206
- # the user's database. Health only needs aggregate counts, so use
2207
- # a read-only snapshot connection instead.
2208
- from superlocalmemory.storage.memory_write import memory_read
2209
-
2210
- db_path = config.db_path
2211
- with memory_read(db_path) as conn:
2212
- row = conn.execute(
2213
- "SELECT COUNT(*), "
2214
- "SUM(CASE WHEN fisher_mean IS NOT NULL THEN 1 ELSE 0 END), "
2215
- "SUM(CASE WHEN langevin_position IS NOT NULL THEN 1 ELSE 0 END) "
2216
- "FROM atomic_facts WHERE profile_id = ?",
2217
- (config.active_profile,),
2218
- ).fetchone()
2219
- total_facts, fisher_count, langevin_count = row or (0, 0, 0)
2220
- facts = [None] * int(total_facts or 0)
2221
- fisher_count = int(fisher_count or 0)
2222
- langevin_count = int(langevin_count or 0)
2223
- else:
2224
- from superlocalmemory.core.engine import MemoryEngine
2225
- engine = MemoryEngine(config)
2226
- engine.initialize()
2227
- facts = engine._db.get_all_facts(engine.profile_id)
2228
- fisher_count = sum(1 for f in facts if f.fisher_mean is not None)
2229
- langevin_count = sum(1 for f in facts if f.langevin_position is not None)
2230
- except Exception as exc:
2231
- if use_json:
2232
- from superlocalmemory.cli.json_output import json_print
2233
- json_print("health", error={"code": "ENGINE_ERROR", "message": str(exc)})
2234
- sys.exit(1)
2235
- raise
2236
-
2237
- if use_json:
2238
- from superlocalmemory.cli.json_output import json_print
2239
- json_print("health", data={
2240
- "total_facts": len(facts),
2241
- "similarity_indexed": fisher_count,
2242
- "lifecycle_positioned": langevin_count,
2243
- "mode": config.mode.value,
2244
- }, next_actions=[
2245
- {"command": "slm status --json", "description": "Check system status"},
2246
- {"command": "slm recall '<query>' --json", "description": "Test retrieval"},
2247
- ])
2248
- return
2249
-
2250
- print("Math Layer Health:")
2251
- print(f" Total facts: {len(facts)}")
2252
- print(f" Math layer indexed: {fisher_count}/{len(facts)}")
2253
- print(f" Lifecycle positioned: {langevin_count}/{len(facts)}")
2254
- print(f" Mode: {config.mode.value.upper()}")
2255
-
2256
-
2257
- def _gather_optimize_surface_b() -> dict:
2258
- """Gather Surface-B health data for slm doctor.
2259
-
2260
- Pure data-gather — never raises, never prints, never starts the
2261
- ConfigStore watchdog thread. Reads daemon-persisted metrics only
2262
- (CacheDB.metrics_load), never the in-process KV counters.
2263
-
2264
- Returns a dict with keys:
2265
- enabled, cache_enabled, compress_enabled, proxy_enabled,
2266
- compress_runs, tokens_saved, cache_hits, cache_misses,
2267
- db_present, error
2268
- """
2269
- from superlocalmemory.infra.data_root import state_path
2270
- from superlocalmemory.optimize.storage.db import CacheDB
2271
-
2272
- result: dict = {
2273
- "enabled": False,
2274
- "cache_enabled": False,
2275
- "compress_enabled": False,
2276
- "proxy_enabled": False,
2277
- "compress_runs": 0,
2278
- "tokens_saved": 0,
2279
- "cache_hits": 0,
2280
- "cache_misses": 0,
2281
- "db_present": False,
2282
- "error": "",
2283
- }
2284
-
2285
- # Step 1: read optimize config — NO watchdog start.
2286
- try:
2287
- from superlocalmemory.optimize.config.store import ConfigStore
2288
- cfg = ConfigStore().get()
2289
- result["enabled"] = cfg.enabled
2290
- result["cache_enabled"] = cfg.cache_enabled
2291
- result["compress_enabled"] = cfg.compress_enabled
2292
- result["proxy_enabled"] = cfg.proxy_enabled
2293
- except Exception as exc: # noqa: BLE001
2294
- result["error"] = str(exc)
2295
- # enabled stays False — safe default
2296
-
2297
- # Step 2: read persisted metrics from llmcache.db (daemon-flushed, ≤60s stale).
2298
- try:
2299
- db_path = state_path("llmcache.db")
2300
- result["db_present"] = db_path.exists()
2301
- if result["db_present"]:
2302
- snap = CacheDB.get_default().metrics_load()
2303
- result["compress_runs"] = snap.compress_runs
2304
- result["tokens_saved"] = snap.tokens_saved_compress
2305
- result["cache_hits"] = snap.hits
2306
- result["cache_misses"] = snap.misses
2307
- except Exception as exc: # noqa: BLE001
2308
- prior = result["error"]
2309
- result["error"] = (prior + "; " if prior else "") + "metrics read failed"
2310
-
2311
- return result
2312
-
2313
-
2314
- def _readline_with_timeout(
2315
- stream, timeout_sec: float,
2316
- ) -> tuple[str | None, Exception | None]:
2317
- """Read one line from a pipe-like stream with a deadline.
2318
-
2319
- Prefer a selector poll of the stream's file descriptor on POSIX so a
2320
- timeout never abandons a thread blocked in readline() (thread + FD leak).
2321
- Windows select() only accepts sockets, not subprocess pipes — fall back
2322
- to a bounded helper thread there and for streams without a usable fileno.
2323
- """
2324
- import selectors
2325
- import threading
2326
-
2327
- timeout_sec = max(0.0, float(timeout_sec))
2328
- fd: int | None
2329
- try:
2330
- raw_fd = stream.fileno()
2331
- fd = raw_fd if isinstance(raw_fd, int) else None
2332
- except (AttributeError, OSError, ValueError, TypeError):
2333
- fd = None
2334
-
2335
- if fd is not None and sys.platform != "win32":
2336
- try:
2337
- with selectors.DefaultSelector() as sel:
2338
- sel.register(fd, selectors.EVENT_READ)
2339
- events = sel.select(timeout=timeout_sec)
2340
- if not events:
2341
- return None, None
2342
- line = stream.readline()
2343
- return (line if isinstance(line, str) else None), None
2344
- except (OSError, ValueError) as exc:
2345
- return None, exc
2346
-
2347
- result: dict[str, object] = {}
2348
-
2349
- def _read() -> None:
2350
- try:
2351
- result["line"] = stream.readline()
2352
- except Exception as exc: # noqa: BLE001 - returned as probe failure
2353
- result["exc"] = exc
2354
-
2355
- reader = threading.Thread(target=_read, daemon=True)
2356
- reader.start()
2357
- reader.join(timeout_sec)
2358
- if reader.is_alive():
2359
- return None, None
2360
- line = result.get("line")
2361
- exc = result.get("exc")
2362
- return (
2363
- line if isinstance(line, str) else None,
2364
- exc if isinstance(exc, Exception) else None,
2365
- )
2366
-
2367
-
2368
- # ---------------------------------------------------------------------------
2369
- # Super-help (v3.8.2) — one place a non-technical user sees EVERYTHING.
2370
- # Grouped so `slm help` is scannable; a drift test asserts every top-level
2371
- # command in the parser appears here (see test_super_help).
2372
- # ---------------------------------------------------------------------------
2373
-
2374
- _COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
2375
- ("Getting started", [
2376
- ("setup", "Guided first-time setup (models, mode, IDEs) — start here"),
2377
- ("init", "Set up + wire Claude Code hooks + connect IDEs"),
2378
- ("reconfigure", "Re-run setup / pick a performance profile"),
2379
- ("mode", "Switch memory mode: a (local) / b (Ollama) / c (cloud)"),
2380
- ("provider", "Configure the cloud LLM provider + API key (Mode C)"),
2381
- ("connect", "Auto-configure detected IDEs (Cursor, VS Code, …)"),
2382
- ("upgrade-hosts", "Preview or explicitly refresh existing SLM host integrations"),
2383
- ("hooks", "Install/inspect Claude Code hooks"),
2384
- ("codex", "Configure the Codex / OpenAI integration"),
2385
- ]),
2386
- ("Store & recall memories", [
2387
- ("remember", "Store a memory (--tags to label)"),
2388
- ("recall", "Semantic + keyword search across memories"),
2389
- ("search", "Exact keyword / full-text search"),
2390
- ("list", "Show recent memories (-n N)"),
2391
- ("update", "Propose an immutable correction by id"),
2392
- ("review-correction", "Apply, reject, or roll back a correction case"),
2393
- ("delete", "Delete a memory by id"),
2394
- ("forget", "Run the decay cycle (preview first)"),
2395
- ("trace", "Recall with a per-channel score breakdown"),
2396
- ("ingest", "Ingest external observations / documents"),
2397
- ("summary", "Readable summaries: session, day, or project"),
2398
- ]),
2399
- ("Privacy & compliance", [
2400
- # gdpr shipped in 4.0.6 but was never listed here, so `slm help` did not
2401
- # mention it at all. The drift test caught it; the omission is the thing
2402
- # that test exists to prevent.
2403
- ("gdpr", "Subject rights: status, export, erase, verify"),
2404
- ]),
2405
- ("Run SLM (daemon & dashboard)", [
2406
- ("serve", "Start/stop the background daemon"),
2407
- ("restart", "Restart the daemon (applies restart-only settings)"),
2408
- ("dashboard", "Open the web dashboard (localhost:8765)"),
2409
- ("mcp", "Start the MCP server (used by IDEs)"),
2410
- ("warmup", "Pre-load models so the first recall is fast"),
2411
- ("status", "Show daemon status + memory counts"),
2412
- ]),
2413
- ("Health & self-healing", [
2414
- ("doctor", "Full pre-flight check (add --fix to auto-repair)"),
2415
- ("health", "Quick math/retrieval layer status"),
2416
- ("ops", "List/resolve stuck, dead-lettered, or degraded operations (admin)"),
2417
- ("diagnostics", "Export a local diagnostics bundle"),
2418
- ("evidence", "Build/inspect evidence bundles"),
2419
- ("rotate-token", "Rotate the local dashboard install token"),
2420
- ]),
2421
- ("Configuration", [
2422
- ("config", "View/set configuration (see: slm help config)"),
2423
- ("enable", "Enable a feature/escape hatch"),
2424
- ("disable", "Disable a feature/escape hatch"),
2425
- ("clear-cache", "Clear caches"),
2426
- ]),
2427
- ("Profiles & sessions", [
2428
- ("profile", "Manage workspaces/profiles (add, switch, list)"),
2429
- ("session", "Open/close a work session"),
2430
- ("session-context", "Pre-stage session context for hooks"),
2431
- ]),
2432
- ("Multi-device mesh", [
2433
- ("mesh", "Inspect/manage the cross-device memory mesh"),
2434
- ]),
2435
- ("Token optimization", [
2436
- ("optimize", "Configure the Optimize surface (cache/compress/proxy)"),
2437
- ("cache", "Manage the LLM response cache"),
2438
- ("compress", "Manage prose compression"),
2439
- ("proxy", "Manage the optimizing LLM proxy"),
2440
- ("wrap", "Run an agent (claude, codex, …) through the proxy"),
2441
- ("help-optimize", "Detailed help for the Optimize surface"),
2442
- ]),
2443
- ("Learning & maintenance", [
2444
- ("evolve", "Skill-evolution controls"),
2445
- ("observe", "External observation / telemetry ingestion"),
2446
- ("brain", "Show profile-scoped Living Brain evidence"),
2447
- ("decay", "Run a forgetting/decay pass"),
2448
- ("consolidate", "Merge/consolidate related memories"),
2449
- ("quantize", "Quantize embeddings to save space"),
2450
- ("soft-prompts", "Manage learned soft prompts"),
2451
- ("reap", "Clean up stale/dead records"),
2452
- ("adapters", "Manage ingestion adapters"),
2453
- ("benchmark", "Run local benchmarks"),
2454
- ]),
2455
- ("Data & migration", [
2456
- ("db", "Low-level database operations"),
2457
- ("migrate", "Migrate the data store to the current version"),
2458
- ]),
2459
- ("Automation", [
2460
- ("loop", "Run gate-verified bounded agent loops"),
2461
- ]),
2462
- ("Help", [
2463
- ("help", "This overview. Try: slm help config | modes | self-heal"),
2464
- ]),
2465
- ]
2466
-
2467
- _HELP_TOPICS: dict[str, str] = {
2468
- "modes": """\
2469
- Operating modes
2470
- a On-device only — no AI language model runs; all data stays on this
2471
- device. Fastest and most private. (EU AI Act: full)
2472
- b On-device + AI — uses a local Ollama model to improve recall quality;
2473
- all data stays on this device. Requires Ollama running.
2474
- c Cloud AI — uses a cloud provider (OpenAI, Anthropic, …) for best
2475
- recall quality; queries leave this device. Needs a key.
2476
-
2477
- Switch any time: slm mode a (or b / c)
2478
- """,
2479
- "config": """\
2480
- Configuration
2481
- slm config Show current configuration
2482
- slm config get <key> Read one setting
2483
- slm config set <key> <val> Change a setting
2484
-
2485
- Most day-to-day settings are editable live in the dashboard
2486
- (Settings tab) and apply without a restart. Settings that need a
2487
- restart show a one-click "apply & restart" banner.
2488
-
2489
- Common runtime settings: recall depth (top_k), memory injection on/off,
2490
- stale-memory suppression, idle consolidation, PII redaction, reranker.
2491
- """,
2492
- "self-heal": """\
2493
- Self-healing (v3.8.2)
2494
- SLM repairs itself. On every start the daemon checks that all models
2495
- and dependencies are present and re-downloads/installs the fixable ones
2496
- in the background — you don't run anything.
2497
-
2498
- See what's present/missing:
2499
- slm doctor Full report with fix commands
2500
- slm doctor --fix Auto-repair everything fixable, then report
2501
- Dashboard → Help → System Health Live "what's missing" report
2502
-
2503
- Big or opt-in pieces (the local LLM 'Ollama', the large compression
2504
- model, PyTorch) are never installed without you — SLM shows the exact
2505
- command instead.
2506
- """,
2507
- }
2508
- # Aliases so common phrasings resolve to a topic.
2509
- _HELP_TOPICS["health"] = _HELP_TOPICS["self-heal"]
2510
- _HELP_TOPICS["selfheal"] = _HELP_TOPICS["self-heal"]
2511
- _HELP_TOPICS["mode"] = _HELP_TOPICS["modes"]
2512
-
2513
-
2514
- def all_help_commands() -> set[str]:
2515
- """Every command name listed in the super-help (drift-test hook)."""
2516
- return {cmd for _title, rows in _COMMAND_GROUPS for cmd, _desc in rows}
2517
-
2518
-
2519
- def cmd_help(args: Namespace) -> None:
2520
- """Super-help: a grouped overview of every command, plus focused topics.
2521
-
2522
- `slm help` → grouped command overview
2523
- `slm help <topic>` → modes | config | self-heal
2524
- """
2525
- topic = getattr(args, "topic", None)
2526
- if topic:
2527
- key = topic.strip().lower()
2528
- text = _HELP_TOPICS.get(key)
2529
- if text:
2530
- print(text)
2531
- else:
2532
- print(f"No help topic '{topic}'. Available topics: "
2533
- + ", ".join(sorted(set(_HELP_TOPICS))))
2534
- return
2535
-
2536
- from superlocalmemory.cli.json_output import _get_version
2537
- print(f"SuperLocalMemory V4 ({_get_version()}) — command overview")
2538
- print("=" * 58)
2539
- print("Run any command with -h for its options, e.g. slm recall -h\n")
2540
- for title, rows in _COMMAND_GROUPS:
2541
- print(f"{title}:")
2542
- for cmd, desc in rows:
2543
- print(f" {cmd:<16} {desc}")
2544
- print()
2545
- print("Health at a glance: slm doctor (auto-repair: slm doctor --fix)")
2546
- print("Topics: slm help modes | slm help config | "
2547
- "slm help self-heal")
2548
- print("Docs: https://superlocalmemory.com")
2549
-
2550
-
2551
- def _migration_error_logs() -> list:
2552
- """Return a sorted list of migration-error log files in the SLM home dir.
2553
-
2554
- Returns an empty list when none exist or when the directory is not readable.
2555
- """
2556
- try:
2557
- from superlocalmemory.core.config import SLMConfig
2558
- from superlocalmemory.infra.data_root import canonical_data_root
2559
- from superlocalmemory.storage.models import Mode
2560
-
2561
- # The daemon writes this log beside the memory database it was actually
2562
- # configured with, which is not necessarily the canonical data root —
2563
- # any of SLM_DATA_DIR / SL_MEMORY_PATH / SLM_HOME can move it. Globbing
2564
- # only the canonical root reported "clean" while the log sat unread in
2565
- # the real directory. Search both, de-duplicated.
2566
- roots = {canonical_data_root()}
2567
- try:
2568
- cfg = SLMConfig.for_mode(Mode.A)
2569
- db = getattr(cfg, "memory_db_path", None) or getattr(cfg, "db_path", None)
2570
- if db:
2571
- roots.add(pathlib.Path(db).parent)
2572
- except Exception:
2573
- pass
2574
-
2575
- found: list = []
2576
- for root in roots:
2577
- try:
2578
- found.extend(root.glob("migration-error-*.log"))
2579
- except OSError:
2580
- continue
2581
- return sorted(set(found))
2582
- except Exception:
2583
- return []
2584
-
2585
-
2586
- def _slm_version() -> str:
2587
- """The installed package version, or "unknown"."""
2588
- try:
2589
- from importlib.metadata import version
2590
- return version("superlocalmemory")
2591
- except Exception: # noqa: BLE001
2592
- return "unknown"
2593
-
2594
-
2595
- def _installed_plugin_versions() -> dict:
2596
- """Version of each editor plugin found on this machine, by install name.
2597
-
2598
- The skills, agents and commands live in the editor's plugin channel rather
2599
- than in the Python package, so upgrading with pip leaves them exactly where
2600
- they were. This looks for them where each editor puts them, and returns an
2601
- empty mapping when none is installed -- which is itself the answer worth
2602
- reporting, because it means pip is the only thing being upgraded.
2603
-
2604
- Best effort by design: an editor this does not know about should produce
2605
- "not detected", never an error.
2606
- """
2607
- import json
2608
- from pathlib import Path
2609
-
2610
- found: dict[str, str] = {}
2611
- roots = (
2612
- # Claude Code: marketplace installs and directly-added plugins.
2613
- Path.home() / ".claude" / "plugins",
2614
- # Codex and VS Code copies, when placed by hand.
2615
- Path.home() / ".codex" / "plugins",
2616
- Path.home() / ".vscode" / "extensions",
2617
- )
2618
- for root in roots:
2619
- if not root.is_dir():
2620
- continue
2621
- for manifest in list(root.glob("*/.claude-plugin/plugin.json")) + \
2622
- list(root.glob("*/plugin.json")) + \
2623
- list(root.glob("*/*/.claude-plugin/plugin.json")):
2624
- try:
2625
- data = json.loads(manifest.read_text(encoding="utf-8"))
2626
- except Exception: # noqa: BLE001 — a sibling plugin's bad json is not ours
2627
- continue
2628
- if str(data.get("name", "")) != "superlocalmemory":
2629
- continue
2630
- found[str(manifest.parent.parent.name)] = str(
2631
- data.get("version", "unknown")
2632
- )
2633
- return found
2634
-
2635
-
2636
- def _detect_all_installs() -> list:
2637
- """Thin shim so cmd_doctor can be tested without importing install_detector."""
2638
- try:
2639
- from superlocalmemory.core.install_detector import (
2640
- _detect_all_installs as _real,
2641
- )
2642
- return _real()
2643
- except Exception:
2644
- return []
2645
-
2646
-
2647
- def cmd_doctor(args: Namespace) -> None:
2648
- """Comprehensive pre-flight check — verify everything works.
2649
-
2650
- S9-UX-10: ``--quick`` skips the slow probes (embedding worker
2651
- subprocess, Ollama roundtrip) so first-run installer hooks can
2652
- surface a sub-second PASS/FAIL line before letting the user go.
2653
- Full doctor remains the default for ``slm doctor`` with no flag.
2654
- """
2655
- import shutil
2656
- from pathlib import Path
2657
-
2658
- use_json = getattr(args, "json", False)
2659
- quick = getattr(args, "quick", False)
2660
- checks: list[dict] = []
2661
- passed = warned = failed = 0
2662
-
2663
- def _check(name: str, status: str, detail: str, fix: str = ""):
2664
- nonlocal passed, warned, failed
2665
- checks.append({"name": name, "status": status, "detail": detail, "fix": fix})
2666
- if status == "PASS":
2667
- passed += 1
2668
- elif status == "WARN":
2669
- warned += 1
2670
- else:
2671
- failed += 1
2672
- if not use_json:
2673
- tag = {"PASS": "[PASS]", "WARN": "[WARN]", "FAIL": "[FAIL]"}[status]
2674
- line = f" {tag} {name}: {detail}"
2675
- if fix:
2676
- line += f"\n Fix: {fix}"
2677
- print(line)
2678
-
2679
- if not use_json:
2680
- print("SuperLocalMemory V4 — Doctor (Pre-flight Check)")
2681
- print("=" * 50)
2682
- print()
2683
-
2684
- # v3.8.2 `--fix`: run the component healer (the SAME repair the daemon
2685
- # self-heal performs) BEFORE the checks, so the report below reflects the
2686
- # healed state. Re-downloads missing models / installs sqlite-vec; big or
2687
- # opt-in items are left as manual fixes. Fail-open — never blocks doctor.
2688
- if getattr(args, "fix", False):
2689
- try:
2690
- from superlocalmemory.core import component_healer
2691
- from superlocalmemory.core.config import SLMConfig as _FixCfg
2692
- try:
2693
- _fcfg = _FixCfg.load()
2694
- except Exception:
2695
- _fcfg = None
2696
- if not use_json:
2697
- print("Auto-repair (--fix): repairing fixable components…")
2698
- _fix_res = component_healer.heal_missing(
2699
- _fcfg,
2700
- on_progress=(None if use_json
2701
- else (lambda k, m: print(f" [{k}] {m}"))),
2702
- )
2703
- if not use_json:
2704
- print(
2705
- f" repaired {_fix_res['healed']}/{_fix_res['attempted']} "
2706
- f"(failed {_fix_res['failed']})\n"
2707
- )
2708
- except Exception as _fix_exc:
2709
- if not use_json:
2710
- print(f" auto-repair error: {_fix_exc}\n")
2711
-
2712
- # H5 — stale-artifact reap (additive; runs after component healer).
2713
- # Removes provably-dead lock/PID files so the doctor report below
2714
- # reflects the healed state. Fail-open — never blocks doctor.
2715
- try:
2716
- from superlocalmemory.infra.self_heal import reap_stale_artifacts
2717
- from superlocalmemory.infra.data_root import canonical_data_root
2718
-
2719
- _sh_report = reap_stale_artifacts(canonical_data_root())
2720
- if not use_json:
2721
- if _sh_report["removed"]:
2722
- print(
2723
- f" Cleaned {len(_sh_report['removed'])} stale"
2724
- " lock/pid file(s):"
2725
- )
2726
- for _item in _sh_report["removed"]:
2727
- print(
2728
- f" - {Path(_item['path']).name}"
2729
- f" ({_item['reason']})"
2730
- )
2731
- else:
2732
- print(" No stale lock/pid files found.")
2733
- print()
2734
- except Exception as _sh_exc:
2735
- if not use_json:
2736
- print(f" stale-artifact cleanup skipped: {_sh_exc}\n")
2737
-
2738
- # 1. Python version
2739
- v = sys.version_info
2740
- if v >= (3, 11):
2741
- _check("Python", "PASS", f"{v.major}.{v.minor}.{v.micro} (>= 3.11)")
2742
- else:
2743
- _check("Python", "FAIL", f"{v.major}.{v.minor}.{v.micro} (need >= 3.11)",
2744
- "Install Python 3.11+ from https://python.org/downloads/")
2745
-
2746
- # 1b. Version integrity (issue #107). Doctor is what a user runs when
2747
- # something seems wrong, so it is exactly where "the code you are running
2748
- # is not the code you installed" has to appear. Reported as WARN rather
2749
- # than FAIL: the installation is sound, it is this *process* that is
2750
- # behind, and `slm doctor` itself is short-lived so it is rarely the
2751
- # stale one -- it is reporting on behalf of the long-lived servers.
2752
- try:
2753
- from superlocalmemory.infra.version_integrity import check_version_integrity
2754
-
2755
- _vi = check_version_integrity()
2756
- if _vi.is_stale:
2757
- _check("Version integrity", "WARN", _vi.detail, _vi.hint)
2758
- elif _vi.state == "mismatch":
2759
- _check("Version integrity", "WARN", _vi.detail, _vi.hint)
2760
- elif _vi.state == "unknown":
2761
- _check("Version integrity", "WARN", _vi.detail,
2762
- "Reinstall so distribution metadata is readable.")
2763
- else:
2764
- _check("Version integrity", "PASS", _vi.detail)
2765
- except Exception as _vi_exc: # noqa: BLE001 - never break doctor
2766
- _check("Version integrity", "WARN", f"could not verify: {_vi_exc}")
2767
-
2768
- # 2. Core deps
2769
- core_modules = {
2770
- "numpy": "numpy", "scipy": "scipy", "networkx": "networkx",
2771
- "httpx": "httpx", "dateutil": "python-dateutil",
2772
- "rank_bm25": "rank-bm25", "vaderSentiment": "vadersentiment",
2773
- "einops": "einops",
2774
- }
2775
- core_ok, core_versions = [], []
2776
- for mod, pkg in core_modules.items():
2777
- try:
2778
- m = __import__(mod)
2779
- ver = getattr(m, "__version__", "?")
2780
- core_ok.append(mod)
2781
- core_versions.append(f"{mod} {ver}")
2782
- except Exception: # dependency import may fail after module discovery
2783
- pass
2784
- if len(core_ok) == len(core_modules):
2785
- _check("Core deps", "PASS", ", ".join(core_versions[:4]) + "...")
2786
- else:
2787
- missing = set(core_modules) - set(core_ok)
2788
- _check("Core deps", "FAIL", f"Missing: {', '.join(missing)}",
2789
- "pip install " + " ".join(core_modules[m] for m in missing))
2790
-
2791
- # 3. Search deps
2792
- #
2793
- # Presence only. Importing these executes torch, which cost ~4 s of every
2794
- # doctor run — a quarter of the whole command — to learn something the
2795
- # embedding-worker check below already proves by actually running them.
2796
- # find_spec answers "is it installed" without executing a line of it.
2797
- # A package that is present but broken on import is caught by check 7,
2798
- # which spawns the worker and waits for it to answer.
2799
- search_mods = {"sentence_transformers": "sentence-transformers", "torch": "torch",
2800
- "sklearn": "scikit-learn"}
2801
- search_ok = []
2802
- for mod, pkg in search_mods.items():
2803
- try:
2804
- import importlib.util as _ilu
2805
- if _ilu.find_spec(mod) is not None:
2806
- search_ok.append(mod)
2807
- except Exception: # a broken meta-path finder must not fail the check
2808
- pass
2809
- if len(search_ok) == len(search_mods):
2810
- _check("Search deps", "PASS", "sentence-transformers, torch, sklearn")
2811
- else:
2812
- missing = set(search_mods) - set(search_ok)
2813
- _check("Search deps", "WARN", f"Missing: {', '.join(missing)}",
2814
- "pip install 'superlocalmemory[search]'")
2815
-
2816
- # 4. Dashboard deps
2817
- dash_ok = True
2818
- for mod in ["fastapi", "uvicorn", "websockets"]:
2819
- try:
2820
- __import__(mod)
2821
- except Exception: # dependency import may fail after module discovery
2822
- dash_ok = False
2823
- break
2824
- if dash_ok:
2825
- _check("Dashboard deps", "PASS", "fastapi, uvicorn, websockets")
2826
- else:
2827
- _check("Dashboard deps", "WARN", "Missing dashboard deps",
2828
- "pip install 'fastapi[all]' uvicorn websockets")
2829
-
2830
- # 5. Learning deps
2831
- try:
2832
- import lightgbm
2833
- _check("Learning deps", "PASS", f"lightgbm {lightgbm.__version__}")
2834
- except Exception: # dependency import may fail after module discovery
2835
- _check("Learning deps", "WARN", "lightgbm not installed",
2836
- "pip install lightgbm")
2837
- except OSError as exc:
2838
- _check("Learning deps", "WARN", f"lightgbm installed but broken: {exc}",
2839
- "brew install libomp && pip install --force-reinstall lightgbm")
2840
-
2841
- # 6. Performance deps
2842
- # v3.4.43: diskcache removed from this check — it was a phantom dependency
2843
- # (declared in pyproject.toml but never imported anywhere in src/ or tests/).
2844
- # Dropping it closes CVE-2025-69872 (pickle deserialization RCE) without any
2845
- # behavior change. orjson remains a real performance dep.
2846
- perf_ok = []
2847
- for mod in ["orjson"]:
2848
- try:
2849
- __import__(mod)
2850
- perf_ok.append(mod)
2851
- except Exception: # dependency import may fail after module discovery
2852
- pass
2853
- if perf_ok:
2854
- _check("Performance deps", "PASS", "orjson")
2855
- else:
2856
- _check("Performance deps", "WARN", "Missing: orjson",
2857
- "pip install orjson")
2858
-
2859
- # 7. Embedding worker functional test — skipped under --quick.
2860
- if quick:
2861
- _check("Embedding worker", "PASS", "skipped (--quick)")
2862
- else:
2863
- try:
2864
- import subprocess as _sp
2865
- import json as _json
2866
-
2867
- env = {
2868
- **__import__("os").environ,
2869
- "CUDA_VISIBLE_DEVICES": "",
2870
- "PYTORCH_MPS_HIGH_WATERMARK_RATIO": "0.0",
2871
- "TOKENIZERS_PARALLELISM": "false",
2872
- "TORCH_DEVICE": "cpu",
2873
- }
2874
- proc = _sp.Popen(
2875
- [sys.executable, "-m",
2876
- "superlocalmemory.core.embedding_worker"],
2877
- stdin=_sp.PIPE, stdout=_sp.PIPE, stderr=_sp.DEVNULL,
2878
- text=True, bufsize=1, env=env,
2879
- )
2880
- proc.stdin.write(_json.dumps({"cmd": "ping"}) + "\n")
2881
- proc.stdin.flush()
2882
-
2883
- line, read_exc = _readline_with_timeout(proc.stdout, 30)
2884
- if read_exc is not None:
2885
- _check("Embedding worker", "FAIL", str(read_exc), "slm warmup")
2886
- elif line is not None:
2887
- resp = _json.loads(line or "{}")
2888
- if resp.get("ok"):
2889
- _check(
2890
- "Embedding worker", "PASS",
2891
- f"responsive (PID {proc.pid}, "
2892
- f"Python {sys.executable})",
2893
- )
2894
- else:
2895
- _check(
2896
- "Embedding worker", "FAIL",
2897
- f"error: {resp.get('error', 'unknown')}",
2898
- "pip install sentence-transformers einops torch",
2899
- )
2900
- else:
2901
- _check("Embedding worker", "FAIL", "timed out (30s)",
2902
- "slm warmup")
2903
- proc.stdin.write(_json.dumps({"cmd": "quit"}) + "\n")
2904
- proc.stdin.flush()
2905
- proc.wait(timeout=5)
2906
- except FileNotFoundError:
2907
- _check(
2908
- "Embedding worker", "FAIL",
2909
- "embedding_worker module not found",
2910
- "Reinstall: npm install -g superlocalmemory",
2911
- )
2912
- except Exception as exc:
2913
- _check("Embedding worker", "FAIL", str(exc), "slm warmup")
2914
-
2915
- # 8. Ollama connectivity (Mode B only) — skipped under --quick.
2916
- if quick:
2917
- _check("Ollama / API key", "PASS", "skipped (--quick)")
2918
- else:
2919
- try:
2920
- from superlocalmemory.core.config import SLMConfig
2921
- config = SLMConfig.load()
2922
- if config.mode.value == "b":
2923
- import httpx
2924
- try:
2925
- resp = httpx.get(
2926
- f"{config.llm.api_base}/api/tags", timeout=5.0,
2927
- )
2928
- if resp.status_code == 200:
2929
- models = [
2930
- m["name"].split(":")[0]
2931
- for m in resp.json().get("models", [])
2932
- ]
2933
- has_llm = config.llm.model.split(":")[0] in models
2934
- if has_llm:
2935
- _check(
2936
- "Ollama", "PASS",
2937
- f"running, {len(models)} models, "
2938
- f"'{config.llm.model}' available",
2939
- )
2940
- else:
2941
- _check(
2942
- "Ollama", "WARN",
2943
- f"running but '{config.llm.model}' "
2944
- f"not pulled",
2945
- f"ollama pull {config.llm.model}",
2946
- )
2947
- else:
2948
- _check(
2949
- "Ollama", "WARN",
2950
- f"HTTP {resp.status_code}",
2951
- "brew services start ollama",
2952
- )
2953
- except Exception:
2954
- _check(
2955
- "Ollama", "WARN",
2956
- "not reachable at " + config.llm.api_base,
2957
- "brew services start ollama",
2958
- )
2959
- elif config.mode.value == "c":
2960
- if config.llm.api_key:
2961
- _check(
2962
- "API key", "PASS",
2963
- f"provider={config.llm.provider}, "
2964
- f"key=***{config.llm.api_key[-4:]}",
2965
- )
2966
- else:
2967
- _check(
2968
- "API key", "WARN", "no API key configured",
2969
- "slm provider set",
2970
- )
2971
- except Exception:
2972
- pass # Config load failed — already caught above
2973
-
2974
- # 9. Disk space
2975
- from superlocalmemory.infra.data_root import canonical_data_root
2976
- slm_home = canonical_data_root()
2977
- try:
2978
- usage = shutil.disk_usage(slm_home if slm_home.exists() else Path.home())
2979
- free_gb = usage.free / (1024 ** 3)
2980
- if free_gb >= 2.0:
2981
- _check("Disk space", "PASS", f"{free_gb:.1f} GB free")
2982
- else:
2983
- _check("Disk space", "WARN", f"{free_gb:.1f} GB free (< 2 GB)",
2984
- "Free up disk space")
2985
- except Exception:
2986
- pass
2987
-
2988
- # 10. Database integrity
2989
- db_path = slm_home / "memory.db"
2990
- if db_path.exists():
2991
- try:
2992
- from superlocalmemory.storage.memory_write import memory_read
2993
-
2994
- # integrity_check reads every page. On a 610 MB store that is
2995
- # 10-15 s — over half of this command — and it ran on every
2996
- # invocation, including the ones a user makes twice in a row while
2997
- # fixing something else. quick_check walks the same B-trees and
2998
- # catches structural damage; what it skips is page-level checksum
2999
- # work that only finds hardware bit rot, which surfaces as read
3000
- # errors long before anyone runs a doctor.
3001
- #
3002
- # The trade is named rather than hidden: the output says which
3003
- # check ran, and --deep runs the exhaustive one.
3004
- deep = bool(getattr(args, "deep", False))
3005
- pragma = "integrity_check" if deep else "quick_check"
3006
- with memory_read(db_path) as conn:
3007
- result = conn.execute(f"PRAGMA {pragma}").fetchone()
3008
- if result and result[0] == "ok":
3009
- size_mb = db_path.stat().st_size / (1024 * 1024)
3010
- detail = f"OK ({size_mb:.2f} MB, {pragma})"
3011
- if not deep:
3012
- detail += " — run `slm doctor --deep` for a full page scan"
3013
- _check("Database", "PASS", detail)
3014
- else:
3015
- _check("Database", "FAIL", f"{pragma}: {result}",
3016
- "Backup and recreate database")
3017
- except Exception as exc:
3018
- _check("Database", "FAIL", str(exc))
3019
- else:
3020
- _check("Database", "PASS", "not yet created (will initialize on first use)")
3021
-
3022
- # 10b. Projection queue. The graph and the vectors live in other storage
3023
- # engines, and a queue that stops draining is the one failure that produces
3024
- # no error at all: the memory is safely in SQLite, nothing raises, and
3025
- # recall quietly stops finding it. Depth alone cannot distinguish a busy
3026
- # queue from a wedged one, so the check keys on attempts — a row that has
3027
- # been tried and refused is a defect with a fact id attached.
3028
- if db_path.exists():
3029
- try:
3030
- from superlocalmemory.storage.memory_write import memory_read
3031
- from superlocalmemory.storage.projection_outbox import DEPTH_SQL
3032
-
3033
- with memory_read(db_path) as conn:
3034
- depth = int(conn.execute(DEPTH_SQL).fetchone()[0])
3035
- stalled = [
3036
- row[0] for row in conn.execute(
3037
- "SELECT fact_id FROM projection_outbox "
3038
- "WHERE attempts >= 3 ORDER BY attempts DESC LIMIT 5"
3039
- )
3040
- ]
3041
- if stalled:
3042
- _check(
3043
- "Projection queue", "FAIL",
3044
- f"{len(stalled)}+ memories refused by the graph or vector "
3045
- f"store (e.g. {', '.join(f[:12] for f in stalled)})",
3046
- "Check `slm logs` for the projection error, then restart "
3047
- "the daemon to retry",
3048
- )
3049
- elif depth:
3050
- _check(
3051
- "Projection queue", "PASS",
3052
- f"{depth} memory/memories queued — the worker drains these "
3053
- "in the background",
3054
- )
3055
- else:
3056
- _check("Projection queue", "PASS", "empty (graph is up to date)")
3057
- except Exception:
3058
- # No table means a store that predates the queue. Nothing is
3059
- # pending on it by definition, so this is not a finding.
3060
- _check("Projection queue", "PASS", "not applicable to this store")
3061
-
3062
- # 11. PEP 668 advisory: detect EXTERNALLY-MANAGED marker and
3063
- # recommend pipx when the system Python is managed by the OS package
3064
- # manager (e.g. Homebrew, Debian/Ubuntu, Fedora 38+).
3065
- try:
3066
- import sysconfig as _sc
3067
- _stdlib = _sc.get_path("stdlib")
3068
- if _stdlib:
3069
- _em_marker = Path(_stdlib) / "EXTERNALLY-MANAGED"
3070
- if _em_marker.exists():
3071
- _check(
3072
- "PEP 668 / Install method",
3073
- "WARN",
3074
- "System Python is externally managed (EXTERNALLY-MANAGED marker found). "
3075
- "pip install may fail with PEP 668 error.",
3076
- "Use an isolated install: pipx install superlocalmemory "
3077
- "or uv tool install superlocalmemory",
3078
- )
3079
- else:
3080
- _check(
3081
- "PEP 668 / Install method",
3082
- "PASS",
3083
- "No EXTERNALLY-MANAGED marker — standard pip install supported",
3084
- )
3085
- except Exception:
3086
- pass # advisory only — never fail doctor on this check
3087
-
3088
- # 12a. Component registry (v3.8.2) — models + optional vector/graph/
3089
- # compression backends the older checks above did NOT cover
3090
- # (embedder/reranker/compressor cache presence, sqlite-vec, lancedb,
3091
- # cozo, llmlingua). Single source of truth shared with the daemon
3092
- # self-heal and the dashboard "what's missing" report. Missing items
3093
- # are advisory (WARN), never FAIL — the self-heal thread and
3094
- # `slm doctor --fix` repair the auto-fixable ones. Runs BEFORE the
3095
- # Optimize check so that check stays last (its ordering is pinned by
3096
- # test_doctor_optimize).
3097
- try:
3098
- from superlocalmemory.core import component_registry as _cr
3099
- try:
3100
- from superlocalmemory.core.config import SLMConfig as _Cfg
3101
- _dcfg = _Cfg.load()
3102
- except Exception:
3103
- _dcfg = None
3104
- _already = {"python", "search_deps", "ollama"}
3105
- for _c in _cr.probe_all(_dcfg):
3106
- if _c.key in _already:
3107
- continue # covered by checks 1/3/8 above — avoid duplicates
3108
- _status = "PASS" if _c.status == _cr.STATUS_OK else "WARN"
3109
- _detail = _c.detail or _c.status
3110
- if _c.auto_fixable:
3111
- _detail += " (auto-heals on daemon start)"
3112
- _check(f"Component: {_c.label}", _status, _detail, _c.fix_cmd)
3113
- except Exception as _cr_exc:
3114
- _check("Component registry", "WARN", f"probe failed: {_cr_exc}")
3115
-
3116
- # 13. Install version convergence — detect all copies of SLM on this machine
3117
- # and warn when they are at different versions. Silent divergence is the
3118
- # most common source of unexpected behaviour after an upgrade.
3119
- try:
3120
- _installs = _detect_all_installs() # shim defined above cmd_doctor
3121
- _versions = {i["version"] for i in _installs}
3122
- if not _installs:
3123
- _check(
3124
- "install_versions",
3125
- "PASS",
3126
- "No additional SLM installs detected on this machine",
3127
- )
3128
- elif len(_versions) > 1:
3129
- _detail = "Version divergence: " + ", ".join(
3130
- f"{i['type']}={i['version']}" for i in _installs
3131
- )
3132
- _check(
3133
- "install_versions",
3134
- "WARN",
3135
- _detail,
3136
- fix="Upgrade all installs: "
3137
- "pipx upgrade superlocalmemory | "
3138
- "pip install -U superlocalmemory (in ~/.slm-venv) | "
3139
- "npm install -g superlocalmemory@latest",
3140
- )
3141
- else:
3142
- _unified = next(iter(_versions))
3143
- _types = ", ".join(i["type"] for i in _installs)
3144
- _check(
3145
- "install_versions",
3146
- "PASS",
3147
- f"All installs at {_unified} ({_types})",
3148
- )
3149
- except Exception as _inst_exc: # noqa: BLE001 — never break doctor
3150
- _check("install_versions", "WARN", f"could not probe installs: {_inst_exc}")
3151
-
3152
- # 14. The skills, agents and commands are NOT in the Python package.
3153
- # They ship through the editor's own plugin channel -- `plugin/` in the
3154
- # repository -- so `pip install --upgrade` cannot move them, and until now
3155
- # nothing said so. 4.1 changed 76 files across those trees; a user who
3156
- # upgraded the package and read a clean `slm doctor` had every reason to
3157
- # believe they had all of it, and no way to find out otherwise.
3158
- try:
3159
- _pkg_version = _slm_version()
3160
- _pl = _installed_plugin_versions()
3161
- if not _pl:
3162
- _check(
3163
- "plugin_skills",
3164
- "WARN",
3165
- f"package is {_pkg_version}; no editor plugin detected, so the "
3166
- f"skills, agents and commands are not installed or updated by "
3167
- f"pip",
3168
- fix="Claude Code: claude plugin marketplace add "
3169
- "qualixar/superlocalmemory && claude plugin install "
3170
- "superlocalmemory@qualixar "
3171
- "Codex / VS Code: copy codex-plugin/ or copilot-plugin/ "
3172
- "from the tag you are on",
3173
- )
3174
- else:
3175
- _stale = {n: v for n, v in _pl.items() if v != _pkg_version}
3176
- if _stale:
3177
- _check(
3178
- "plugin_skills",
3179
- "WARN",
3180
- "package is %s; plugin content still at %s" % (
3181
- _pkg_version,
3182
- ", ".join(f"{n}={v}" for n, v in sorted(_stale.items())),
3183
- ),
3184
- fix="claude plugin marketplace update qualixar && "
3185
- "claude plugin update superlocalmemory@qualixar",
3186
- )
3187
- else:
3188
- _check(
3189
- "plugin_skills",
3190
- "PASS",
3191
- f"plugin content matches the package ({_pkg_version})",
3192
- )
3193
- except Exception as _pl_exc: # noqa: BLE001 — never break doctor
3194
- _check("plugin_skills", "WARN", f"could not probe plugins: {_pl_exc}")
3195
-
3196
- # 14. Migration error logs — surface any unresolved failure from a previous
3197
- # upgrade attempt. The daemon writes these and they persist until the
3198
- # user takes action; doctor is the right place to surface them.
3199
- try:
3200
- _err_logs = _migration_error_logs()
3201
- if _err_logs:
3202
- _latest = _err_logs[-1]
3203
- _check(
3204
- "migration_errors",
3205
- "FAIL",
3206
- f"Unresolved migration failure: {_latest.name}",
3207
- # `--fix` heals models, sqlite-vec and stale locks. It does NOT
3208
- # re-run a migration or restore a snapshot, so pointing here at
3209
- # `--fix` sent the user round a loop that could never clear this
3210
- # check. The log itself carries the restore command.
3211
- fix=(
3212
- f"Read {_latest} — it names what failed and, if a snapshot "
3213
- f"was taken, the exact command to restore it. Restart the "
3214
- f"daemon to retry the upgrade. Delete the log once resolved."
3215
- ),
3216
- )
3217
- else:
3218
- # Reporting nothing when clean made "no failures" and "the check
3219
- # never ran" look identical in the output.
3220
- _check("migration_errors", "PASS", "no unresolved migration failures")
3221
- except Exception as _mel_exc: # noqa: BLE001 — never break doctor
3222
- _check("migration_errors", "WARN", f"could not check error logs: {_mel_exc}")
3223
-
3224
- # 12. Optimize (Surface B) — reads daemon-persisted metrics (≤60s stale).
3225
- info = _gather_optimize_surface_b()
3226
- _enabled = info["enabled"]
3227
- _error = info.get("error", "")
3228
- if not _enabled:
3229
- _check(
3230
- "Optimize (Surface B)",
3231
- "WARN",
3232
- "disabled (optimize.json enabled=false) — caching/compression not active"
3233
- + (f" [{_error}]" if _error else ""),
3234
- fix="Enable via dashboard Optimize tab or set enabled=true"
3235
- f" in {slm_home / 'optimize.json'}",
3236
- )
3237
- else:
3238
- _surfaces = []
3239
- if info["cache_enabled"]:
3240
- _surfaces.append("cache")
3241
- if info["compress_enabled"]:
3242
- _surfaces.append("compress")
3243
- if info["proxy_enabled"]:
3244
- _surfaces.append("proxy")
3245
- _stats = (
3246
- f"compress_runs={info['compress_runs']}"
3247
- f" tokens_saved={info['tokens_saved']}"
3248
- f" cache_hits={info['cache_hits']}"
3249
- f" cache_misses={info['cache_misses']}"
3250
- )
3251
- _surface_str = ",".join(_surfaces) if _surfaces else "(none)"
3252
- if not _surfaces:
3253
- _check(
3254
- "Optimize (Surface B)",
3255
- "WARN",
3256
- f"enabled [{_surface_str}] but no surface active"
3257
- + (f" [{_error}]" if _error else ""),
3258
- )
3259
- elif not info["db_present"]:
3260
- _check(
3261
- "Optimize (Surface B)",
3262
- "WARN",
3263
- f"enabled [{_surface_str}] {_stats}"
3264
- " but no metrics yet (llmcache.db not created)"
3265
- + (f" [{_error}]" if _error else ""),
3266
- fix="slm serve start",
3267
- )
3268
- elif _error:
3269
- _check(
3270
- "Optimize (Surface B)",
3271
- "WARN",
3272
- f"enabled [{_surface_str}] {_stats} (partial: {_error})",
3273
- )
3274
- else:
3275
- _check(
3276
- "Optimize (Surface B)",
3277
- "PASS",
3278
- f"enabled [{_surface_str}] {_stats}",
3279
- )
3280
-
3281
- # Memory answer-ability.
3282
- #
3283
- # Every other check here asks whether a component is installed. None of
3284
- # them asked the only question that matters to the owner: can my memories
3285
- # actually be found? One machine ran for months with 43.7% of its store
3286
- # unreachable while doctor reported everything green, because the reachable
3287
- # share was never counted.
3288
- memory_health_data: dict | None = None
3289
- try:
3290
- from superlocalmemory.core.config import SLMConfig as _HealthCfg
3291
- from superlocalmemory.core.memory_health import describe, measure
3292
- _h = measure(_HealthCfg.load().db_path)
3293
- memory_health_data = {
3294
- "live_facts": _h.live_facts,
3295
- "findable_by_meaning": _h.findable_by_meaning,
3296
- "missing_vector": _h.missing_vector,
3297
- "withheld_summaries": _h.withheld_summaries,
3298
- "display_summaries": _h.display_summaries,
3299
- "hidden_by_forgetting": _h.hidden_by_forgetting,
3300
- "inconsistently_hidden": _h.inconsistently_hidden,
3301
- "reachability": round(_h.reachability, 4),
3302
- "healthy": _h.healthy,
3303
- "unavailable": list(_h.unavailable),
3304
- "summary": describe(_h),
3305
- }
3306
- _detail = " ".join(describe(_h))
3307
- if _h.healthy:
3308
- _check("Memory answer-ability", "PASS", _detail)
3309
- elif _h.inconsistently_hidden or _h.reachability < 0.9:
3310
- _check(
3311
- "Memory answer-ability", "FAIL", _detail,
3312
- fix="slm restart",
3313
- )
3314
- else:
3315
- _check("Memory answer-ability", "WARN", _detail,
3316
- fix="slm db reembed --missing-only")
3317
- except Exception as _mh_exc: # noqa: BLE001 — a report must not break doctor
3318
- _check(
3319
- "Memory answer-ability", "WARN",
3320
- f"could not be measured: {_mh_exc}",
3321
- )
3322
-
3323
- # Summary
3324
- if use_json:
3325
- from superlocalmemory.cli.json_output import json_print
3326
- next_actions = []
3327
- for c in checks:
3328
- if c["fix"]:
3329
- next_actions.append({"command": c["fix"], "description": f"Fix {c['name']}"})
3330
- json_print("doctor", data={
3331
- "checks": checks,
3332
- "summary": {"passed": passed, "warned": warned, "failed": failed},
3333
- "memory_health": memory_health_data,
3334
- }, next_actions=next_actions)
3335
- else:
3336
- print(f"\nSummary: {passed} passed, {warned} warnings, {failed} failed")
3337
- if failed > 0:
3338
- print("Run the suggested fix commands above, then re-run: slm doctor")
3339
-
3340
-
3341
- def cmd_trace(args: Namespace) -> None:
3342
- """Recall with per-channel score breakdown."""
3343
- use_json = getattr(args, 'json', False)
3344
- limit = getattr(args, 'limit', 10)
3345
-
3346
- # Trace must use the same daemon-owned engine as recall. A direct CLI
3347
- # engine cannot attach to the machine-wide embedding worker already owned
3348
- # by the daemon; it then silently loses semantic, Hopfield, and spreading
3349
- # activation channels. The daemon trace route keeps the loaded model,
3350
- # graph, and retrieval state intact while returning the same score detail.
3351
- try:
3352
- from superlocalmemory.cli.daemon import (
3353
- daemon_request, ensure_daemon, is_daemon_running,
3354
- )
3355
- if is_daemon_running() or ensure_daemon():
3356
- result = daemon_request(
3357
- "POST", "/api/v3/recall/trace",
3358
- {"query": args.query, "limit": limit},
3359
- )
3360
- if result and "results" in result:
3361
- if use_json:
3362
- from superlocalmemory.cli.json_output import json_print
3363
- json_print("trace", data={
3364
- "query": result.get("query", args.query),
3365
- "query_type": result.get("query_type", "unknown"),
3366
- "retrieval_time_ms": round(
3367
- float(result.get("retrieval_time_ms", 0)), 1,
3368
- ),
3369
- "results": result["results"],
3370
- "count": len(result["results"]),
3371
- "no_confident_match": bool(
3372
- result.get("no_confident_match", False),
3373
- ),
3374
- "score_contract_version": result.get(
3375
- "score_contract_version", "2",
3376
- ),
3377
- "calibration_status": result.get(
3378
- "calibration_status", "uncalibrated",
3379
- ),
3380
- "calibration_id": result.get("calibration_id"),
3381
- "answer_confidence": result.get("answer_confidence"),
3382
- "abstained": bool(result.get("abstained", False)),
3383
- "abstention_reason": result.get("abstention_reason"),
3384
- }, next_actions=[
3385
- {
3386
- "command": "slm recall '<query>' --json",
3387
- "description": "Standard recall",
3388
- },
3389
- ])
3390
- return
3391
- print(f"Query: {result.get('query', args.query)}")
3392
- print(
3393
- f"Type: {result.get('query_type', 'unknown')} | Time: "
3394
- f"{float(result.get('retrieval_time_ms', 0)):.0f}ms"
3395
- )
3396
- print(f"Results: {len(result['results'])}")
3397
- for i, item in enumerate(result["results"], 1):
3398
- print(
3399
- f"\n {i}. [relevance "
3400
- f"{float(item.get('relevance_score', item.get('score', 0))):.3f}] "
3401
- f"{str(item.get('content', ''))[:100]}"
3402
- )
3403
- if item.get("ranking_score") is not None:
3404
- print(
3405
- " ranking utility: "
3406
- f"{float(item['ranking_score']):.6f}"
3407
- )
3408
- for channel, score in (item.get("channel_scores") or {}).items():
3409
- print(f" {channel}: {float(score):.3f}")
3410
- return
3411
- except Exception:
3412
- # The direct path remains the offline escape hatch when a daemon is
3413
- # unavailable or a local transport error occurs.
3414
- pass
3415
-
3416
- from superlocalmemory.core.engine import MemoryEngine
3417
- from superlocalmemory.core.config import SLMConfig
3418
- try:
3419
- config = SLMConfig.load()
3420
- engine = MemoryEngine(config)
3421
- engine.initialize()
3422
- response = engine.recall(args.query, limit=limit)
3423
- except Exception as exc:
3424
- if use_json:
3425
- from superlocalmemory.cli.json_output import json_print
3426
- json_print("trace", error={"code": "ENGINE_ERROR", "message": str(exc)})
3427
- sys.exit(1)
3428
- raise
3429
-
3430
- if use_json:
3431
- from superlocalmemory.cli.json_output import json_print
3432
- from superlocalmemory.server.recall_serializer import (
3433
- recall_response_metadata,
3434
- serialize_recall_response,
3435
- )
3436
- items, no_confident_match = serialize_recall_response(
3437
- response,
3438
- limit=limit,
3439
- per_fact_max=200,
3440
- total_max=max(200, limit * 200),
3441
- )
3442
- json_print("trace", data={
3443
- "query": args.query,
3444
- "query_type": getattr(response, "query_type", "unknown"),
3445
- "retrieval_time_ms": round(getattr(response, "retrieval_time_ms", 0), 1),
3446
- "results": items, "count": len(items),
3447
- "no_confident_match": no_confident_match,
3448
- **recall_response_metadata(response),
3449
- }, next_actions=[
3450
- {"command": "slm recall '<query>' --json", "description": "Standard recall"},
3451
- ])
3452
- return
3453
-
3454
- print(f"Query: {args.query}")
3455
- print(f"Type: {response.query_type} | Time: {response.retrieval_time_ms:.0f}ms")
3456
- print(f"Results: {len(response.results)}")
3457
- for i, r in enumerate(response.results, 1):
3458
- print(f"\n {i}. [relevance {r.relevance_score:.3f}] {r.fact.content[:100]}")
3459
- if r.ranking_score is not None:
3460
- print(f" ranking utility: {r.ranking_score:.6f}")
3461
- if hasattr(r, "channel_scores") and r.channel_scores:
3462
- for ch, sc in r.channel_scores.items():
3463
- print(f" {ch}: {sc:.3f}")
3464
-
3465
-
3466
- # -- Services (no --json — these start long-running processes) -------------
3467
-
3468
-
3469
- def cmd_mcp(_args: Namespace) -> None:
3470
- """Start the V3 MCP server (stdio transport for IDE integration)."""
3471
- # SINGLETON GUARD (v3.5.8): Reap fresh orphans before binding stdio.
3472
- # Root cause: every IDE session spawns a new `slm mcp`; dead sessions'
3473
- # processes survive indefinitely (~400 MB each) because the reaper's
3474
- # 1-hour age gate misses them. Fix: run find_orphans with age_threshold=0
3475
- # so any `slm mcp` whose parent IDE/Claude process has died is killed
3476
- # immediately. Only orphans (dead parent) are killed — live sessions safe.
3477
- # CRITICAL: No stdout anywhere below — MCP stdio transport, any print
3478
- # corrupts the JSON-RPC protocol.
3479
- try:
3480
- from superlocalmemory.infra.process_reaper import (
3481
- ReaperConfig,
3482
- find_orphans,
3483
- is_mcp_server_process,
3484
- kill_orphan,
3485
- )
3486
- _reaper_cfg = ReaperConfig(orphan_age_threshold_hours=0.0)
3487
- for _orphan in find_orphans(_reaper_cfg):
3488
- # A unified daemon is expected to be detached from the launching
3489
- # shell and can therefore have PPID 1. The MCP reaper must never
3490
- # treat that healthy shared daemon as an orphaned stdio server.
3491
- if is_mcp_server_process(_orphan):
3492
- kill_orphan(_orphan.pid, graceful_timeout_seconds=1.0)
3493
- except Exception:
3494
- pass # Never block MCP startup on cleanup failure
3495
-
3496
- # Version integrity (issue #107). The reaper above only kills *orphans* —
3497
- # servers whose parent died. A server whose IDE is still alive is never an
3498
- # orphan, so it survives upgrades indefinitely and keeps serving the code
3499
- # it imported at startup. That is how eighteen `slm mcp` processes spanning
3500
- # four days and two releases stayed alive on one machine, and how a stale
3501
- # one made issue #106 look unfixed across two debugging sessions.
3502
- #
3503
- # This runs at startup, so a server launched *after* an upgrade is correct
3504
- # by construction and stays silent. It fires for a server that started
3505
- # before its own package was upgraded — which is possible when the client
3506
- # respawns it from an old cached path.
3507
- #
3508
- # CRITICAL: logging only, never stdout — MCP speaks JSON-RPC over stdio and
3509
- # any print corrupts the protocol. The logger writes to stderr.
3510
- try:
3511
- from superlocalmemory.infra.version_integrity import check_version_integrity
3512
-
3513
- _vi = check_version_integrity()
3514
- if _vi.is_stale:
3515
- logger.warning("MCP server version drift: %s. %s", _vi.detail, _vi.hint)
3516
- elif _vi.differs:
3517
- logger.info("MCP server version note: %s", _vi.detail)
3518
- except Exception:
3519
- pass # A diagnostic must never prevent the server from starting.
3520
-
3521
- from superlocalmemory.mcp.server import server
3522
-
3523
- server.run(transport="stdio")
3524
-
3525
-
3526
- def cmd_warmup(_args: Namespace) -> None:
3527
- """Pre-download the embedding model so first use is instant.
3528
-
3529
- v3.4.42: daemon-aware. The embedding worker is a machine-wide
3530
- singleton (`_is_embedding_worker_alive` + PID file), so when the
3531
- unified daemon is running it OWNS the worker. A fresh
3532
- `EmbeddingService` started here would see the singleton, set
3533
- `_available = False`, return None from `_subprocess_embed`, and
3534
- print "embedding verification failed" — even though the daemon's
3535
- worker is already happily serving the same model. The fix: detect
3536
- the daemon, verify via its health endpoint, and skip the local
3537
- spawn. Only fall through to the original local-worker path when
3538
- the daemon is genuinely unreachable.
3539
- """
3540
- import superlocalmemory.core.embeddings as _emb_mod
3541
-
3542
- print("SuperLocalMemory V4 — Embedding Model Warmup")
3543
- print("=" * 50)
3544
- print(f" Python: {sys.executable}")
3545
- print(f" Model: nomic-ai/nomic-embed-text-v1.5 (~500MB)")
3546
- print()
3547
-
3548
- # v3.4.42 — daemon-aware fast path. If the daemon is up and reports
3549
- # engine=initialized, the embedding model is already loaded inside
3550
- # the daemon's worker subprocess. No need to spawn a redundant one;
3551
- # in fact, the machine-wide singleton would refuse to do so anyway.
3552
- try:
3553
- from superlocalmemory.cli.daemon import (
3554
- is_daemon_running, daemon_request,
3555
- )
3556
- if is_daemon_running():
3557
- health = daemon_request("GET", "/health")
3558
- if health and health.get("engine") == "initialized":
3559
- from superlocalmemory.core.config import EmbeddingConfig
3560
- cfg = EmbeddingConfig()
3561
- print("[PASS] Daemon is running with embedding model loaded.")
3562
- print(f" Model: {cfg.model_name} ({cfg.dimension}-dim)")
3563
- print("Semantic search is fully operational.")
3564
- return
3565
- # Daemon up but engine not yet initialized — warn and return
3566
- # rather than racing the daemon for the singleton lock.
3567
- engine_state = (health or {}).get("engine", "unknown")
3568
- print(f"[INFO] Daemon is up but engine state is '{engine_state}'.")
3569
- print(" Wait ~30s and retry, or run: slm doctor")
3570
- return
3571
- except Exception:
3572
- # Any failure in the daemon path falls through to local warmup —
3573
- # better to spawn a local worker than block warmup entirely.
3574
- pass
3575
-
3576
- # Local-warmup fallback path: daemon is unreachable, so it's safe
3577
- # to spawn our own embedding worker (no singleton conflict).
3578
- # Increase timeout for first-time download.
3579
- original_timeout = _emb_mod._SUBPROCESS_RESPONSE_TIMEOUT
3580
- _emb_mod._SUBPROCESS_RESPONSE_TIMEOUT = 180 # 3 min for cold start
3581
-
3582
- try:
3583
- from superlocalmemory.core.config import EmbeddingConfig
3584
- from superlocalmemory.core.embeddings import EmbeddingService
3585
-
3586
- config = EmbeddingConfig()
3587
-
3588
- print("Step 1/3: Spawning embedding worker subprocess...")
3589
- svc = EmbeddingService(config)
3590
-
3591
- if not svc.is_available:
3592
- print("\n[FAIL] Embedding service not available.")
3593
- _warmup_diagnose()
3594
- return
3595
-
3596
- print("Step 2/3: Loading model (may download ~500MB on first run)...")
3597
- emb = svc.embed("warmup test")
3598
-
3599
- if emb and len(emb) == config.dimension:
3600
- print("Step 3/3: Verifying embedding output...")
3601
- print(f"\n[PASS] Model ready: {config.model_name} ({config.dimension}-dim)")
3602
- print("Semantic search is fully operational.")
3603
- else:
3604
- print("\n[FAIL] Model loaded but embedding verification failed.")
3605
- _warmup_diagnose()
3606
-
3607
- except ImportError as exc:
3608
- print(f"\n[FAIL] Missing dependency: {exc}")
3609
- print("Fix: pip install sentence-transformers einops torch")
3610
- except Exception as exc:
3611
- print(f"\n[FAIL] Warmup failed: {exc}")
3612
- _warmup_diagnose()
3613
- finally:
3614
- _emb_mod._SUBPROCESS_RESPONSE_TIMEOUT = original_timeout
3615
-
3616
-
3617
- def _warmup_diagnose() -> None:
3618
- """Diagnostic helper when warmup fails."""
3619
- print("\nDiagnosing...")
3620
- print(f" Python executable: {sys.executable}")
3621
- os.environ["ORT_DISABLE_COREML"] = "1"
3622
- try:
3623
- from sentence_transformers import SentenceTransformer
3624
- print(" sentence-transformers: importable")
3625
- m = SentenceTransformer(
3626
- "nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True, device="cpu",
3627
- )
3628
- v = m.encode(["test"], normalize_embeddings=True)
3629
- print(f" Direct embed: OK (dim={v.shape[1]})")
3630
- print("\n Issue: Subprocess worker failed but direct import works.")
3631
- print(" This is likely a Python path mismatch between Node.js wrapper")
3632
- print(" and your current shell. Run: slm doctor")
3633
- except ImportError as ie:
3634
- print(f" sentence-transformers: NOT importable ({ie})")
3635
- print(" Fix: pip install sentence-transformers einops torch")
3636
- except Exception as de:
3637
- print(f" Direct embed failed: {de}")
3638
- print(" Run: slm doctor")
3639
-
3640
-
3641
- def cmd_dashboard(args: Namespace) -> None:
3642
- """Open the web dashboard in the browser.
3643
-
3644
- v3.4.3: Dashboard is now served by the unified daemon. This command
3645
- ensures the daemon is running and opens the browser. It does NOT
3646
- start a separate server (saves ~500MB RAM from duplicate engine).
3647
- """
3648
- from superlocalmemory.cli.daemon import ensure_daemon, _get_port
3649
-
3650
- port = getattr(args, "port", None) or _get_port()
3651
-
3652
- print(" SuperLocalMemory V4 — Web Dashboard")
3653
- print(f" Starting daemon if needed...")
3654
-
3655
- if not ensure_daemon(port=port):
3656
- print(" ✗ Could not start daemon. Run `slm doctor` to diagnose.")
3657
- sys.exit(1)
3658
-
3659
- url = f"http://localhost:{port}"
3660
- print(f" ✓ Daemon running")
3661
- print(f" Dashboard: {url}")
3662
- print(f" API Docs: {url}/docs")
3663
-
3664
- # Open browser
3665
- import webbrowser
3666
- webbrowser.open(url)
3667
- print("\n Dashboard opened in browser. Daemon continues running in background.")
3668
-
3669
-
3670
- # -- Profiles (supports --json) -------------------------------------------
3671
-
3672
-
3673
- def _switch_profile_runtime(config, profile_name: str) -> dict:
3674
- """Switch through the resident daemon, or persist an offline fallback."""
3675
- from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
3676
-
3677
- if is_daemon_running():
3678
- result = daemon_request(
3679
- "POST",
3680
- f"/api/profiles/{profile_name}/switch",
3681
- )
3682
- if not result or not result.get("success"):
3683
- raise RuntimeError(
3684
- "resident daemon did not acknowledge the profile switch"
3685
- )
3686
- acknowledged = str(result.get("active_profile", ""))
3687
- if acknowledged != profile_name:
3688
- raise RuntimeError(
3689
- "resident daemon acknowledged a different active profile"
3690
- )
3691
- return {
3692
- "action": "switched",
3693
- "profile": acknowledged,
3694
- "generation": int(result.get("generation", 0)),
3695
- "runtime": "daemon",
3696
- }
3697
-
3698
- from superlocalmemory.server.profile_runtime import persist_active_profile
3699
-
3700
- persist_active_profile(profile_name)
3701
- config.active_profile = profile_name
3702
- return {
3703
- "action": "switched",
3704
- "profile": profile_name,
3705
- "runtime": "offline",
3706
- }
3707
-
3708
-
3709
- def cmd_profile(args: Namespace) -> None:
3710
- """Profile management (list, switch, create).
3711
-
3712
- Writes to BOTH SQLite and profiles.json so CLI, Dashboard, and
3713
- MCP all see the same profiles.
3714
- """
3715
- action = getattr(args, "action", "list")
3716
- if action in ("switch", "create"):
3717
- from superlocalmemory.core.admission import gate_cli_mutation
3718
- from superlocalmemory.core.operation_request import OperationKind
3719
- # The role that decides this is the one held on the workspace being
3720
- # switched to or created, not on whichever one happens to be active.
3721
- gate_cli_mutation(
3722
- OperationKind.PROFILE_SWITCH,
3723
- profile=str(getattr(args, "name", "") or ""),
3724
- )
3725
-
3726
- from superlocalmemory.core.config import SLMConfig
3727
- from superlocalmemory.storage.database import DatabaseManager
3728
- from superlocalmemory.storage import schema
3729
- from superlocalmemory.server.routes.helpers import (
3730
- ensure_profile_in_json,
3731
- )
3732
-
3733
- config = SLMConfig.load()
3734
- db = DatabaseManager(config.db_path)
3735
- db.initialize(schema)
3736
-
3737
- if getattr(args, 'json', False):
3738
- from superlocalmemory.cli.json_output import json_print
3739
- if args.action == "list":
3740
- rows = db.execute("SELECT profile_id, name FROM profiles")
3741
- profiles = [
3742
- {"profile_id": dict(r)["profile_id"], "name": dict(r).get("name", "")}
3743
- for r in rows
3744
- ]
3745
- json_print("profile", data={"profiles": profiles, "count": len(profiles)},
3746
- next_actions=[
3747
- {"command": "slm profile switch <name> --json", "description": "Switch profile"},
3748
- ])
3749
- elif args.action == "switch":
3750
- rows = db.execute(
3751
- "SELECT 1 FROM profiles WHERE profile_id = ?",
3752
- (args.name,),
3753
- )
3754
- if not rows:
3755
- json_print("profile", error={
3756
- "code": "PROFILE_NOT_FOUND",
3757
- "message": f"Profile '{args.name}' does not exist.",
3758
- })
3759
- sys.exit(1)
3760
- try:
3761
- result = _switch_profile_runtime(config, args.name)
3762
- except Exception as exc:
3763
- json_print("profile", error={
3764
- "code": "PROFILE_SWITCH_FAILED",
3765
- "message": str(exc),
3766
- })
3767
- sys.exit(1)
3768
- json_print("profile", data=result)
3769
- elif args.action == "create":
3770
- db.execute(
3771
- "INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
3772
- (args.name, args.name),
3773
- )
3774
- ensure_profile_in_json(args.name)
3775
- json_print("profile", data={"action": "created", "profile": args.name},
3776
- next_actions=[
3777
- {"command": f"slm profile switch {args.name} --json",
3778
- "description": "Switch to new profile"},
3779
- ])
3780
- return
3781
-
3782
- if args.action == "list":
3783
- rows = db.execute("SELECT profile_id, name FROM profiles")
3784
- print("Profiles:")
3785
- for r in rows:
3786
- d = dict(r)
3787
- print(f" - {d['profile_id']}: {d.get('name', '')}")
3788
- elif args.action == "switch":
3789
- rows = db.execute(
3790
- "SELECT 1 FROM profiles WHERE profile_id = ?",
3791
- (args.name,),
3792
- )
3793
- if not rows:
3794
- print(f"Profile '{args.name}' does not exist.", file=sys.stderr)
3795
- sys.exit(1)
3796
- try:
3797
- result = _switch_profile_runtime(config, args.name)
3798
- except Exception as exc:
3799
- print(f"Profile switch failed: {exc}", file=sys.stderr)
3800
- sys.exit(1)
3801
- generation = result.get("generation")
3802
- suffix = f" (generation {generation})" if generation is not None else ""
3803
- print(f"Switched to profile: {args.name}{suffix}")
3804
- elif args.action == "create":
3805
- db.execute(
3806
- "INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
3807
- (args.name, args.name),
3808
- )
3809
- ensure_profile_in_json(args.name)
3810
- print(f"Created profile: {args.name}")
3811
-
3812
-
3813
- # -- Active Memory commands (V3.1) ------------------------------------------
3814
-
3815
-
3816
- def _cmd_init_auto(
3817
- args: Namespace,
3818
- slm_data_dir: "Path",
3819
- config_exists: bool,
3820
- force: bool,
3821
- ) -> None:
3822
- """Non-interactive --auto branch for slm init.
3823
-
3824
- Best-effort at every step; only exits non-zero when config save fails.
3825
- No TTY required. Does NOT run IDE connect (AC6).
3826
- """
3827
- from pathlib import Path
3828
- from superlocalmemory.core.config import SLMConfig
3829
- from superlocalmemory.storage.models import Mode
3830
-
3831
- # Step 1: write mode-A config (create-if-absent or --force).
3832
- # Pass slm_data_dir explicitly so env-overridden paths are respected even
3833
- # when DEFAULT_BASE_DIR was evaluated before the env var was set (e.g. tests).
3834
- if force or not config_exists:
3835
- try:
3836
- cfg = SLMConfig.for_mode(Mode.A, base_dir=slm_data_dir)
3837
- cfg.save(mode_change=True)
3838
- except Exception as exc:
3839
- print(f"[ERROR] slm init --auto: config save failed: {exc}", file=sys.stderr)
3840
- sys.exit(1)
3841
-
3842
- # Step 2: mark complete (write .setup-complete sentinel).
3843
- # Write the sentinel directly using the already-selected namespace.
3844
- try:
3845
- import platform
3846
- import time as _time
3847
- sentinel = slm_data_dir / ".setup-complete"
3848
- slm_data_dir.mkdir(parents=True, exist_ok=True)
3849
- sentinel.write_text(
3850
- f"setup_completed={_time.strftime('%Y-%m-%dT%H:%M:%S')}\n"
3851
- f"python={sys.executable}\n"
3852
- f"platform={platform.system()}\n"
3853
- f"version={platform.python_version()}\n"
3854
- )
3855
- except Exception:
3856
- pass # best-effort — sentinel is advisory only
3857
-
3858
- # Step 3: install hooks if ~/.claude exists.
3859
- try:
3860
- claude_dir = Path.home() / ".claude"
3861
- if claude_dir.exists():
3862
- from superlocalmemory.hooks.claude_code_hooks import install_hooks
3863
- install_hooks(include_gate=getattr(args, "gate", False))
3864
- except Exception:
3865
- pass # best-effort
3866
-
3867
- # Step 4: warmup best-effort (don't block if models not present).
3868
- # Skipped in --auto to keep startup fast for CI; user can run slm warmup.
3869
-
3870
- print("[OK] slm init --auto: setup complete (mode A, non-interactive)", file=sys.stderr)
3871
-
3872
-
3873
- def cmd_init(args: Namespace) -> None:
3874
- """One-command setup: mode + hooks + IDE connect + warmup."""
3875
- from superlocalmemory.cli._lazy_init import slm_home
3876
- from superlocalmemory.core.config import SLMConfig
3877
-
3878
- force = getattr(args, "force", False)
3879
- auto = getattr(args, "auto", False)
3880
-
3881
- slm_data_dir = slm_home()
3882
- config_exists = (slm_data_dir / "config.json").exists()
3883
-
3884
- # --auto branch — fully non-interactive, no TTY required.
3885
- if auto:
3886
- os.environ["SLM_NON_INTERACTIVE"] = "1"
3887
- _cmd_init_auto(args, slm_data_dir, config_exists, force)
3888
- return
3889
-
3890
- print()
3891
- print("SuperLocalMemory — One-Time Setup")
3892
- print("=" * 40)
3893
-
3894
- # Step 1: Mode selection (interactive)
3895
- if force or not config_exists:
3896
- print()
3897
- from superlocalmemory.cli.setup_wizard import run_wizard
3898
- run_wizard()
3899
- else:
3900
- config = SLMConfig.load()
3901
- print(f"\n Already configured: Mode {config.mode.value.upper()}")
3902
- print(f" Profile: {config.active_profile}")
3903
-
3904
- # Step 2: Install hooks (gate always OFF by default)
3905
- print()
3906
- print("Installing Claude Code hooks...")
3907
- from superlocalmemory.hooks.claude_code_hooks import install_hooks, check_status
3908
-
3909
- status = check_status()
3910
-
3911
- if status["installed"] and not force:
3912
- if status["needs_upgrade"]:
3913
- from superlocalmemory.hooks.claude_code_hooks import upgrade_hooks
3914
- result = upgrade_hooks()
3915
- if result.get("upgraded"):
3916
- print(f" Hooks upgraded: {result['from_version']} -> {result['to_version']}")
3917
- else:
3918
- print(f" Upgrade issue: {result.get('reason', result.get('errors', ''))}")
3919
- else:
3920
- print(f" Hooks already installed (v{status['version']})")
3921
- else:
3922
- result = install_hooks(include_gate=False)
3923
- if result["success"]:
3924
- print(f" Hooks installed: {', '.join(result['hooks_added'])}")
3925
- print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
3926
- else:
3927
- print(f" Hook install failed: {result['errors']}")
3928
-
3929
- # Step 3: IDE connection
3930
- print()
3931
- print("Detecting IDEs...")
3932
- try:
3933
- from superlocalmemory.hooks.ide_connector import IDEConnector
3934
- connector = IDEConnector()
3935
- results = connector.connect_all()
3936
- for ide_id, ide_status in results.items():
3937
- print(f" {ide_id}: {ide_status}")
3938
- except Exception as exc:
3939
- print(f" IDE detection skipped: {exc}")
3940
-
3941
- # Step 4: Warmup (embedding model)
3942
- print()
3943
- print("Checking embedding model...")
3944
- try:
3945
- from superlocalmemory.core.config import SLMConfig as _Cfg
3946
- cfg = _Cfg.load()
3947
- model_name = cfg.embedding.model_name
3948
- print(f" Model: {model_name}")
3949
- # Quick check: try creating embedding service (auto-downloads if needed)
3950
- from superlocalmemory.core.embeddings import EmbeddingService
3951
- svc = EmbeddingService(cfg.embedding)
3952
- test_result = svc.embed_text("test")
3953
- if test_result is not None and len(test_result) > 0:
3954
- print(" Status: ready")
3955
- else:
3956
- print(" Status: model not available (run: slm warmup)")
3957
- except Exception as exc:
3958
- print(f" Warmup skipped: {exc}")
3959
- print(" Run 'slm warmup' later to download the embedding model.")
3960
-
3961
- # Done
3962
- print()
3963
- print("=" * 40)
3964
- print("SLM is active. Your AI now remembers you.")
3965
- print()
3966
- print("What happens next:")
3967
- print(" - Open Claude Code in any project")
3968
- print(" - SLM auto-injects your memory context")
3969
- print(" - Decisions, bugs, preferences are captured automatically")
3970
- print(" - Session summaries saved when you close")
3971
- print()
3972
-
3973
-
3974
- def cmd_hooks(args: Namespace) -> None:
3975
- """Manage additive Claude Code or Codex memory lifecycle hooks."""
3976
- from superlocalmemory.hooks.claude_code_hooks import (
3977
- install_hooks, remove_hooks, check_status,
3978
- )
3979
-
3980
- action = getattr(args, "action", "status")
3981
- agent = getattr(args, "agent", "claude")
3982
- dry_run = getattr(args, "dry_run", False)
3983
- if agent == "codex":
3984
- from superlocalmemory.hooks.codex_hooks import (
3985
- install_hooks as install_codex_hooks,
3986
- remove_hooks as remove_codex_hooks,
3987
- check_status as check_codex_hooks,
3988
- )
3989
- if action == "install":
3990
- result = install_codex_hooks(dry_run=dry_run)
3991
- if result["success"]:
3992
- prefix = "would be installed" if dry_run else "installed"
3993
- print(f"SLM hooks {prefix} in Codex: {result['path']}")
3994
- if result.get("hooks_added"):
3995
- print(f" Hook types: {', '.join(result['hooks_added'])}")
3996
- print(" Review and trust the new hooks in Codex with /hooks.")
3997
- else:
3998
- print(f"Installation failed: {result['errors']}")
3999
- return
4000
- if action == "remove":
4001
- result = remove_codex_hooks(dry_run=dry_run)
4002
- if result["success"]:
4003
- prefix = "would be removed" if dry_run else "removed"
4004
- print(f"SLM hooks {prefix} from Codex: {result['path']}")
4005
- else:
4006
- print(f"Removal failed: {result['errors']}")
4007
- return
4008
- result = check_codex_hooks()
4009
- if result["installed"] is None:
4010
- print(f"SLM Codex hooks: INDETERMINATE ({result['error']})")
4011
- elif result["installed"]:
4012
- print("SLM Codex hooks: INSTALLED")
4013
- print(f" Hook types: {', '.join(result['hook_types'])}")
4014
- else:
4015
- print("SLM Codex hooks: NOT INSTALLED")
4016
- print(" Run: slm hooks install --agent codex")
4017
- return
4018
- # Gate is OFF by default. --gate opts in (for brave users).
4019
- include_gate = getattr(args, "gate", False)
4020
-
4021
- if action == "install":
4022
- result = install_hooks(include_gate=include_gate)
4023
- if result["success"]:
4024
- print("SLM hooks installed in Claude Code.")
4025
- print(f" Hook types: {', '.join(result['hooks_added'])}")
4026
- if include_gate:
4027
- print(" Gate: ON (enforces session_init — experimental)")
4028
- print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
4029
-
4030
- else:
4031
- print(f"Installation failed: {result['errors']}")
4032
- elif action == "remove":
4033
- result = remove_hooks()
4034
- if result["success"]:
4035
- print("SLM hooks removed from Claude Code.")
4036
- else:
4037
- print(f"Removal failed: {result['errors']}")
4038
- else:
4039
- result = check_status()
4040
- if result["installed"]:
4041
- print(f"SLM hooks: INSTALLED (v{result['version']})")
4042
- print(f" Hook types: {', '.join(result['hook_types'])}")
4043
- print(f" Gate: {'ON' if result['gate_enabled'] else 'OFF'}")
4044
- if result["needs_upgrade"]:
4045
- print(f" Update available: {result['version']} -> {result['latest_version']}")
4046
- print(" Run: slm hooks install")
4047
- else:
4048
- print("SLM hooks: NOT INSTALLED")
4049
- print(" Run: slm hooks install")
4050
- print(" Or: slm init (full setup)")
4051
-
4052
-
4053
- def cmd_codex(args: Namespace) -> None:
4054
- """Manage explicit, SLM-owned Codex skills, agents, and lifecycle hooks."""
4055
- from superlocalmemory.hooks.codex_assets import install_assets, remove_assets, status_assets
4056
- from superlocalmemory.hooks.codex_hooks import install_hooks, remove_hooks, check_status
4057
-
4058
- action, dry_run = getattr(args, "action", "status"), getattr(args, "dry_run", False)
4059
- if action == "install":
4060
- assets, hooks = install_assets(dry_run=dry_run), install_hooks(dry_run=dry_run)
4061
- if assets.get("success") and hooks.get("success"):
4062
- print(f"SLM Codex add-ons {'would be installed' if dry_run else 'installed'}: 7 skills, 2 subagents, 4 lifecycle hooks.")
4063
- print("MCP wiring remains explicit: run `slm connect codex` if it is not already configured.")
4064
- print("Review and trust newly installed hooks in Codex with /hooks.")
4065
- else:
4066
- print(f"Codex integration failed: {assets.get('errors', []) + hooks.get('errors', [])}")
4067
- return
4068
- if action == "remove":
4069
- assets, hooks = remove_assets(dry_run=dry_run), remove_hooks(dry_run=dry_run)
4070
- if assets.get("success") and hooks.get("success"):
4071
- print("SLM-owned Codex add-ons removed; your MCP and non-SLM settings were left intact.")
4072
- else:
4073
- print(f"Codex removal failed: {assets.get('errors', []) + hooks.get('errors', [])}")
4074
- return
4075
- assets, hooks = status_assets(), check_status()
4076
- print(f"SLM Codex add-ons: {'INSTALLED' if assets['installed'] and hooks['installed'] else 'NOT INSTALLED'}")
4077
- print(f" Skills: {len(assets['skills'])}/7; subagents: {len(assets['agents'])}/2")
4078
- print(f" Hooks: {', '.join(hooks.get('hook_types', [])) or 'none'}")
4079
-
4080
-
4081
- def cmd_session_context(args: Namespace) -> None:
4082
- """Print session context (for hook scripts and piping).
4083
-
4084
- Uses a FAST PATH that queries SQLite directly (no engine/Ollama needed).
4085
- This ensures the SessionStart hook completes within its 15s timeout even
4086
- when Ollama requires a 60s+ cold start. The fast path returns:
4087
- - Core Memory blocks (always-on context)
4088
- - Recent high-importance memories (last N days)
4089
- - Session summary from last session
4090
- Falls back to the full engine path only if --full is passed explicitly.
4091
-
4092
- v3.4.65: uses shared injection formatter (render_context) for identical
4093
- output across MCP and CLI surfaces. --json flag returns structured JSON.
4094
- """
4095
- import sqlite3
4096
- from superlocalmemory.core.config import SLMConfig
4097
-
4098
- use_json = getattr(args, "json", False)
4099
- use_full = getattr(args, "full", False)
4100
-
4101
- if use_full:
4102
- # Full engine path (slow, requires Ollama) — for explicit CLI use
4103
- try:
4104
- from superlocalmemory.hooks.auto_recall import AutoRecall
4105
- from superlocalmemory.core.engine import MemoryEngine
4106
- config = SLMConfig.load()
4107
- engine = MemoryEngine(config)
4108
- engine.initialize()
4109
- auto = AutoRecall(
4110
- engine=engine,
4111
- config={"enabled": True, "max_memories_injected": 10, "relevance_threshold": 0.3},
4112
- )
4113
- context = auto.get_session_context(
4114
- query=getattr(args, "query", "") or "recent decisions and important context",
4115
- )
4116
- if context:
4117
- if use_json:
4118
- from superlocalmemory.cli.json_output import json_print
4119
- json_print("session-context", data={"context": context}, next_actions=[
4120
- {"command": "slm recall --json <query>", "description": "Search memories"},
4121
- ])
4122
- else:
4123
- print(context)
4124
- except Exception as exc:
4125
- logger.debug("session-context (full) failed: %s", exc)
4126
- return
4127
-
4128
- # ── FAST PATH: direct SQLite, no engine, <500ms ──────────────
4129
- try:
4130
- from superlocalmemory.core.injection import InjectableMemory, render_context
4131
-
4132
- config = SLMConfig.load()
4133
- db_path = config.base_dir / "memory.db"
4134
- if not db_path.exists():
4135
- return
4136
-
4137
- pid = config.active_profile
4138
- from superlocalmemory.storage.memory_write import memory_read
4139
-
4140
- def _read_rows(query: str, params: tuple[object, ...]) -> list[sqlite3.Row]:
4141
- with memory_read(db_path) as conn:
4142
- return conn.execute(query, params).fetchall()
4143
-
4144
- # Collect facts for injection — same queries as pre-v3.4.65 but
4145
- # mapped into InjectableMemory for the shared formatter.
4146
- inj_mems: list[InjectableMemory] = []
4147
-
4148
- # Core Memory blocks (compiled high-value context)
4149
- try:
4150
- cm_rows = _read_rows(
4151
- "SELECT block_type, content FROM core_memory_blocks "
4152
- "WHERE profile_id = ? ORDER BY block_type",
4153
- (pid,),
4154
- )
4155
- for r in cm_rows:
4156
- content = f"[{r['block_type']}] {r['content']}"
4157
- inj_mems.append(InjectableMemory(
4158
- content=content, score=1.0, fact_id="",
4159
- importance=1.0, access_count=10,
4160
- pinned=True,
4161
- ))
4162
- except sqlite3.OperationalError:
4163
- pass
4164
-
4165
- # Recent important memories — age gate from --max-age-days (default 30)
4166
- max_age = getattr(args, "max_age_days", 30)
4167
- age_clause = (
4168
- f"AND created_at >= datetime('now', '-{int(max_age)} days') "
4169
- if max_age > 0 else ""
4170
- )
4171
- try:
4172
- fact_rows = _read_rows(
4173
- "SELECT fact_id, content, importance, access_count, fact_type FROM atomic_facts "
4174
- "WHERE profile_id = ? "
4175
- f"{age_clause}"
4176
- "AND lifecycle = 'active' "
4177
- "ORDER BY importance DESC, created_at DESC LIMIT 10",
4178
- (pid,),
4179
- )
4180
- for r in fact_rows:
4181
- inj_mems.append(InjectableMemory(
4182
- content=r["content"],
4183
- score=r["importance"] or 0.5,
4184
- fact_id=r["fact_id"],
4185
- importance=r["importance"] or 0.0,
4186
- access_count=r["access_count"] or 0,
4187
- ))
4188
- except sqlite3.OperationalError:
4189
- pass
4190
-
4191
- # Session markers (last session summary)
4192
- try:
4193
- sess_rows = _read_rows(
4194
- "SELECT fact_id, content, importance, access_count FROM atomic_facts "
4195
- "WHERE profile_id = ? AND content LIKE 'Session%' "
4196
- "ORDER BY created_at DESC LIMIT 3",
4197
- (pid,),
4198
- )
4199
- for r in sess_rows:
4200
- inj_mems.append(InjectableMemory(
4201
- content=r["content"],
4202
- score=r["importance"] or 0.3,
4203
- fact_id=r["fact_id"],
4204
- importance=r["importance"] or 0.0,
4205
- access_count=r["access_count"] or 0,
4206
- ))
4207
- except sqlite3.OperationalError:
4208
- pass
4209
-
4210
- if not inj_mems:
4211
- return
4212
-
4213
- # V3.3 Soft prompts (auto-learned patterns) — append as high-importance
4214
- try:
4215
- sp_rows = _read_rows(
4216
- "SELECT category, content FROM soft_prompt_templates "
4217
- "WHERE profile_id = ? AND active = 1 "
4218
- "ORDER BY confidence DESC LIMIT 5",
4219
- (pid,),
4220
- )
4221
- for r in sp_rows:
4222
- inj_mems.append(InjectableMemory(
4223
- content=f"[{r['category']}] {r['content']}",
4224
- score=0.7, fact_id="",
4225
- importance=0.7, access_count=5,
4226
- ))
4227
- except Exception:
4228
- pass
4229
-
4230
- cfg_inj = getattr(config, "injection", None)
4231
- context = render_context(inj_mems, mode=config.mode.value.upper(), cfg=cfg_inj, wrap=True)
4232
- if context:
4233
- if use_json:
4234
- from superlocalmemory.cli.json_output import json_print
4235
- json_print("session-context", data={
4236
- "context": context,
4237
- "memory_count": len(inj_mems),
4238
- "mode": config.mode.value,
4239
- }, next_actions=[
4240
- {"command": "slm recall --json <query>", "description": "Search memories"},
4241
- ])
4242
- else:
4243
- print(context)
4244
-
4245
- except Exception as exc:
4246
- logger.debug("session-context (fast) failed: %s", exc)
4247
-
4248
-
4249
- def cmd_brain(args: Namespace) -> None:
4250
- """Read the portable, profile-scoped Living Brain truth snapshot.
4251
-
4252
- This command deliberately opens no memory engine and starts no daemon.
4253
- BrainTruth uses short read-only connections to keep the status view out of
4254
- recall and writer domains.
4255
- """
4256
- from superlocalmemory.brain.truth import BrainTruthService
4257
- from superlocalmemory.core.config import SLMConfig
4258
- from superlocalmemory.infra.data_root import state_path
4259
-
4260
- config = SLMConfig.load()
4261
- profile_id = config.active_profile
4262
- data = BrainTruthService(
4263
- memory_db_path=state_path("memory.db"),
4264
- learning_db_path=state_path("learning.db"),
4265
- ).snapshot(profile_id)
4266
- if getattr(args, "json", False):
4267
- from superlocalmemory.cli.json_output import json_print
4268
-
4269
- json_print("brain", data=data, next_actions=[
4270
- {"command": "slm dashboard", "description": "Open the Living Brain dashboard"},
4271
- ])
4272
- return
4273
- memory = data["memory_activity"]
4274
- feedback = data["feedback"]
4275
- evidence = data["agent_experience"]
4276
- external = data["external_evidence"]
4277
- corrections = data["correction_quality"]
4278
- print("SuperLocalMemory Living Brain")
4279
- print(f" Profile: {profile_id}")
4280
- print(f" Stored facts: {memory['facts_total']}")
4281
- print(f" Feedback signals: {feedback['signals_total']}")
4282
- print(f" Claimed agent experiences: {evidence['claimed_experiences_total']}")
4283
- print(
4284
- " Independently verified experiences: "
4285
- f"{evidence['independently_verified_experiences_total']}"
4286
- )
4287
- print(f" Cognitive turns: {evidence['cognitive_turns_total']}")
4288
- if evidence["cognitive_turns_by_state"]:
4289
- states = ", ".join(
4290
- f"{state}: {count}"
4291
- for state, count in sorted(evidence["cognitive_turns_by_state"].items())
4292
- )
4293
- print(f" Turn states: {states}")
4294
- print(f" Bounded Loop observations: {external['receipts_total']}")
4295
- print(f" Correction cases: {corrections['cases_total']}")
4296
- print(
4297
- " Retrieval control: observation only; does not change recall, "
4298
- "ranking, or model routing"
4299
- )
4300
-
4301
-
4302
- def cmd_observe(args: Namespace) -> None:
4303
- """Evaluate and auto-capture content from stdin or argument.
4304
-
4305
- V3.3.28: Routes through daemon to prevent embedding worker memory blast.
4306
- Previously each `slm observe` spawned its own MemoryEngine + embedding
4307
- worker (~1.4 GB each). With 20 parallel edits = 28+ GB = system crash.
4308
- Now uses the daemon's singleton engine (1 worker total).
4309
- """
4310
- import sys
4311
-
4312
- content = getattr(args, "content", "") or ""
4313
- if not content and not sys.stdin.isatty():
4314
- content = sys.stdin.read().strip()
4315
-
4316
- if not content:
4317
- print("No content to observe.")
4318
- return
4319
-
4320
- from superlocalmemory.core.admission import gate_cli_mutation
4321
- from superlocalmemory.core.operation_request import OperationKind
4322
- gate_cli_mutation(OperationKind.REMEMBER)
4323
-
4324
- # V3.3.28: Route through daemon (singleton engine, single embedding worker).
4325
- # This is the P0 fix for the memory blast incident of April 7, 2026.
4326
- from superlocalmemory.cli.daemon import DaemonRefused
4327
- try:
4328
- from superlocalmemory.cli.daemon import is_daemon_running, daemon_request, ensure_daemon
4329
- if is_daemon_running() or ensure_daemon():
4330
- result = daemon_request("POST", "/observe", {"content": content})
4331
- if result is not None:
4332
- if result.get("captured"):
4333
- cat = result.get("category", "unknown")
4334
- conf = result.get("confidence", 0)
4335
- print(f"Auto-captured: {cat} (confidence: {conf:.2f}) (via daemon)")
4336
- else:
4337
- reason = result.get("reason", "no patterns matched")
4338
- print(f"Not captured: {reason}")
4339
- return
4340
- except DaemonRefused as refusal:
4341
- # The workspace declined this write. The fallback below exists for a
4342
- # daemon that is not running -- not for one that answered no.
4343
- print(
4344
- f"[slm] Not captured: {refusal}. "
4345
- "This workspace requires authentication; set SLM_USER_SESSION or "
4346
- "log in, then try again.",
4347
- flush=True,
4348
- )
4349
- sys.exit(1)
4350
- except Exception:
4351
- pass # Fall through to direct engine
4352
-
4353
- # Fallback: direct engine (only if daemon unavailable).
4354
- # Acquires a system-wide file lock to prevent concurrent worker spawns.
4355
- try:
4356
- from superlocalmemory.hooks.auto_capture import AutoCapture
4357
- from superlocalmemory.core.config import SLMConfig
4358
- from superlocalmemory.core.engine import MemoryEngine
4359
- from superlocalmemory.core.embeddings import acquire_embedding_lock
4360
-
4361
- if not acquire_embedding_lock():
4362
- logger.debug("observe: another embedding worker active, skipping")
4363
- print("Not captured: system busy (another embedding in progress)")
4364
- return
4365
-
4366
- try:
4367
- config = SLMConfig.load()
4368
- engine = MemoryEngine(config)
4369
- engine.initialize()
4370
-
4371
- from superlocalmemory.core.engine_ingestion import (
4372
- canonical_store_fn,
4373
- local_trusted_actor_id,
4374
- )
4375
- auto = AutoCapture(store_fn=canonical_store_fn(
4376
- engine,
4377
- source_type="cli-observe",
4378
- trusted_actor_id=local_trusted_actor_id("cli"),
4379
- ))
4380
- decision = auto.evaluate(content)
4381
-
4382
- if decision.capture:
4383
- stored = auto.capture(content, category=decision.category)
4384
- if stored:
4385
- print(f"Auto-captured: {decision.category} (confidence: {decision.confidence:.2f})")
4386
- else:
4387
- print(f"Detected {decision.category} but store failed.")
4388
- else:
4389
- print(f"Not captured: {decision.reason}")
4390
- finally:
4391
- from superlocalmemory.core.embeddings import release_embedding_lock
4392
- release_embedding_lock()
4393
- except Exception as exc:
4394
- logger.debug("observe failed: %s", exc)
4395
-
4396
-
4397
- # -- V3.3 Commands -----------------------------------------------------------
4398
-
4399
-
4400
- def cmd_decay(args: Namespace) -> None:
4401
- """Run Ebbinghaus forgetting decay cycle."""
4402
- from superlocalmemory.core.admission import gate_cli_mutation
4403
- from superlocalmemory.core.operation_request import OperationKind
4404
- gate_cli_mutation(OperationKind.CONSOLIDATE)
4405
- from superlocalmemory.core.config import SLMConfig
4406
- from superlocalmemory.core.engine import MemoryEngine
4407
-
4408
- use_json = getattr(args, "json", False)
4409
- dry_run = getattr(args, "dry_run", True)
4410
- profile = getattr(args, "profile", "")
4411
-
4412
- try:
4413
- config = SLMConfig.load()
4414
- engine = MemoryEngine(config)
4415
- engine.initialize()
4416
- pid = profile or engine.profile_id
4417
-
4418
- from superlocalmemory.math.ebbinghaus import EbbinghausCurve
4419
- from superlocalmemory.learning.forgetting_scheduler import (
4420
- ForgettingScheduler,
4421
- )
4422
-
4423
- ebbinghaus = EbbinghausCurve(config.forgetting)
4424
- scheduler = ForgettingScheduler(
4425
- engine._db, ebbinghaus, config.forgetting,
4426
- )
4427
- result = scheduler.run_decay_cycle(pid, force=True, dry_run=dry_run)
4428
- except Exception as exc:
4429
- if use_json:
4430
- from superlocalmemory.cli.json_output import json_print
4431
- json_print("decay", error={"code": "DECAY_ERROR", "message": str(exc)})
4432
- sys.exit(1)
4433
- raise
4434
-
4435
- if use_json:
4436
- from superlocalmemory.cli.json_output import json_print
4437
- json_print("decay", data={"dry_run": dry_run, **result},
4438
- next_actions=[
4439
- {"command": "slm decay --execute --json", "description": "Apply transitions"},
4440
- {"command": "slm status --json", "description": "Check system status"},
4441
- ])
4442
- return
4443
-
4444
- if result.get("skipped"):
4445
- print(f"Skipped: {result.get('reason', 'unknown')}")
4446
- return
4447
-
4448
- total = result.get("total", 0)
4449
- print(f"Decay cycle complete (dry_run={dry_run})")
4450
- print(f" Total facts: {total}")
4451
- print(f" Active: {result.get('active', 0)}")
4452
- print(f" Warm: {result.get('warm', 0)}")
4453
- print(f" Cold: {result.get('cold', 0)}")
4454
- print(f" Archive: {result.get('archive', 0)}")
4455
- print(f" Forgotten: {result.get('forgotten', 0)}")
4456
- print(f" Transitions: {result.get('transitions', 0)}")
4457
-
4458
-
4459
- def cmd_quantize(args: Namespace) -> None:
4460
- """Run EAP embedding quantization cycle."""
4461
- from superlocalmemory.core.admission import gate_cli_mutation
4462
- from superlocalmemory.core.operation_request import OperationKind
4463
- gate_cli_mutation(OperationKind.CONSOLIDATE)
4464
- from superlocalmemory.core.config import SLMConfig
4465
- from superlocalmemory.core.engine import MemoryEngine
4466
-
4467
- use_json = getattr(args, "json", False)
4468
- dry_run = getattr(args, "dry_run", True)
4469
- profile = getattr(args, "profile", "")
4470
-
4471
- try:
4472
- config = SLMConfig.load()
4473
- engine = MemoryEngine(config)
4474
- engine.initialize()
4475
- pid = profile or engine.profile_id
4476
-
4477
- from superlocalmemory.math.ebbinghaus import EbbinghausCurve
4478
- from superlocalmemory.dynamics.eap_scheduler import EAPScheduler
4479
- from superlocalmemory.storage.quantized_store import (
4480
- QuantizedEmbeddingStore,
4481
- )
4482
-
4483
- from superlocalmemory.math.polar_quant import PolarQuantEncoder
4484
- from superlocalmemory.math.qjl import QJLEncoder
4485
-
4486
- ebbinghaus = EbbinghausCurve(config.forgetting)
4487
- polar = PolarQuantEncoder(config.quantization.polar)
4488
- qjl = QJLEncoder(config.quantization.qjl)
4489
- qstore = QuantizedEmbeddingStore(
4490
- engine._db, polar, qjl, config.quantization,
4491
- )
4492
- scheduler = EAPScheduler(
4493
- engine._db, ebbinghaus, qstore, config.quantization,
4494
- )
4495
- result = scheduler.run_eap_cycle(pid, dry_run=dry_run)
4496
- except Exception as exc:
4497
- if use_json:
4498
- from superlocalmemory.cli.json_output import json_print
4499
- json_print("quantize", error={"code": "EAP_ERROR", "message": str(exc)})
4500
- sys.exit(1)
4501
- raise
4502
-
4503
- if use_json:
4504
- from superlocalmemory.cli.json_output import json_print
4505
- json_print("quantize", data={"dry_run": dry_run, **result},
4506
- next_actions=[
4507
- {"command": "slm quantize --execute --json", "description": "Apply changes"},
4508
- {"command": "slm status --json", "description": "Check status"},
4509
- ])
4510
- return
4511
-
4512
- print(f"EAP quantization cycle (dry_run={dry_run})")
4513
- print(f" Total: {result.get('total', 0)}")
4514
- print(f" Downgrades: {result.get('downgrades', 0)}")
4515
- print(f" Upgrades: {result.get('upgrades', 0)}")
4516
- print(f" Skipped: {result.get('skipped', 0)}")
4517
- print(f" Errors: {result.get('errors', 0)}")
4518
-
4519
-
4520
- def cmd_consolidate(args: Namespace) -> None:
4521
- """Run cognitive consolidation pipeline."""
4522
- from superlocalmemory.core.admission import gate_cli_mutation
4523
- from superlocalmemory.core.operation_request import OperationKind
4524
- gate_cli_mutation(OperationKind.CONSOLIDATE)
4525
-
4526
- from superlocalmemory.core.config import SLMConfig
4527
- from superlocalmemory.core.engine import MemoryEngine
4528
-
4529
- use_json = getattr(args, "json", False)
4530
- cognitive = getattr(args, "cognitive", False)
4531
- dry_run = getattr(args, "dry_run", False)
4532
- profile = getattr(args, "profile", "")
4533
-
4534
- if not cognitive:
4535
- if use_json:
4536
- from superlocalmemory.cli.json_output import json_print
4537
- json_print("consolidate", error={
4538
- "code": "MISSING_FLAG",
4539
- "message": "Use --cognitive to run CCQ pipeline",
4540
- })
4541
- sys.exit(1)
4542
- print("Use --cognitive to run CCQ consolidation pipeline.")
4543
- print(" slm consolidate --cognitive")
4544
- return
4545
-
4546
- try:
4547
- config = SLMConfig.load()
4548
- engine = MemoryEngine(config)
4549
- engine.initialize()
4550
- pid = profile or engine.profile_id
4551
-
4552
- from superlocalmemory.encoding.cognitive_consolidator import (
4553
- CognitiveConsolidator,
4554
- )
4555
-
4556
- consolidator = CognitiveConsolidator(db=engine._db)
4557
- result = consolidator.run_pipeline(pid, dry_run=dry_run)
4558
- except Exception as exc:
4559
- if use_json:
4560
- from superlocalmemory.cli.json_output import json_print
4561
- json_print("consolidate", error={
4562
- "code": "CCQ_ERROR", "message": str(exc),
4563
- })
4564
- sys.exit(1)
4565
- raise
4566
-
4567
- if use_json:
4568
- from superlocalmemory.cli.json_output import json_print
4569
- json_print("consolidate", data={
4570
- "clusters_processed": result.clusters_processed,
4571
- "blocks_created": result.blocks_created,
4572
- "facts_archived": result.facts_archived,
4573
- "compression_ratio": round(result.compression_ratio, 3),
4574
- }, next_actions=[
4575
- {"command": "slm list --json", "description": "List recent memories"},
4576
- {"command": "slm status --json", "description": "Check status"},
4577
- ])
4578
- return
4579
-
4580
- print("CCQ Cognitive Consolidation")
4581
- print(f" Clusters processed: {result.clusters_processed}")
4582
- print(f" Blocks created: {result.blocks_created}")
4583
- print(f" Facts archived: {result.facts_archived}")
4584
- print(f" Compression ratio: {result.compression_ratio:.3f}")
4585
-
4586
-
4587
- def cmd_soft_prompts(args: Namespace) -> None:
4588
- """List active soft prompts (auto-learned user patterns)."""
4589
- from superlocalmemory.core.config import SLMConfig
4590
- from superlocalmemory.core.engine import MemoryEngine
4591
-
4592
- use_json = getattr(args, "json", False)
4593
- profile = getattr(args, "profile", "")
4594
-
4595
- try:
4596
- config = SLMConfig.load()
4597
- engine = MemoryEngine(config)
4598
- engine.initialize()
4599
- pid = profile or engine.profile_id
4600
-
4601
- rows = engine._db.execute(
4602
- "SELECT prompt_id, category, content, confidence, "
4603
- " effectiveness, token_count, version, created_at "
4604
- "FROM soft_prompt_templates "
4605
- "WHERE profile_id = ? AND active = 1 "
4606
- "ORDER BY confidence DESC",
4607
- (pid,),
4608
- )
4609
- prompts = []
4610
- for row in rows:
4611
- r = dict(row)
4612
- prompts.append({
4613
- "prompt_id": r["prompt_id"],
4614
- "category": r["category"],
4615
- "content": r["content"],
4616
- "confidence": round(float(r["confidence"]), 3),
4617
- "effectiveness": round(float(r["effectiveness"]), 3),
4618
- "token_count": int(r["token_count"]),
4619
- "version": int(r["version"]),
4620
- "created_at": r["created_at"],
4621
- })
4622
- except Exception as exc:
4623
- if use_json:
4624
- from superlocalmemory.cli.json_output import json_print
4625
- json_print("soft-prompts", error={
4626
- "code": "QUERY_ERROR", "message": str(exc),
4627
- })
4628
- sys.exit(1)
4629
- raise
4630
-
4631
- if use_json:
4632
- from superlocalmemory.cli.json_output import json_print
4633
- json_print("soft-prompts", data={
4634
- "prompts": prompts, "count": len(prompts), "profile": pid,
4635
- }, next_actions=[
4636
- {"command": "slm status --json", "description": "Check status"},
4637
- ])
4638
- return
4639
-
4640
- if not prompts:
4641
- print("No active soft prompts.")
4642
- return
4643
-
4644
- print(f"Active soft prompts ({len(prompts)}):\n")
4645
- for i, p in enumerate(prompts, 1):
4646
- print(f" {i}. [{p['category']}] (conf={p['confidence']:.2f})")
4647
- content_preview = p["content"][:100]
4648
- if len(p["content"]) > 100:
4649
- content_preview += "..."
4650
- print(f" {content_preview}")
4651
-
4652
-
4653
- def cmd_reap(args: Namespace) -> None:
4654
- """Find and kill orphaned SLM processes."""
4655
- use_json = getattr(args, "json", False)
4656
- dry_run = not getattr(args, "force", False)
4657
- # V3.5.9: --all bypasses orphan detection and kills every slm mcp process
4658
- # except the current one. Use after switching IDEs to clear stale sessions.
4659
- use_force_all = getattr(args, "all", False)
4660
- if use_force_all:
4661
- dry_run = False # --all always kills; --force is implied
4662
-
4663
- try:
4664
- from superlocalmemory.infra.process_reaper import (
4665
- cleanup_all_orphans,
4666
- ReaperConfig,
4667
- )
4668
-
4669
- config = ReaperConfig()
4670
- result = cleanup_all_orphans(config, dry_run=dry_run, force=use_force_all)
4671
- except Exception as exc:
4672
- if use_json:
4673
- from superlocalmemory.cli.json_output import json_print
4674
- json_print("reap", error={
4675
- "code": "REAP_ERROR", "message": str(exc),
4676
- })
4677
- sys.exit(1)
4678
- raise
4679
-
4680
- if use_json:
4681
- from superlocalmemory.cli.json_output import json_print
4682
- json_print("reap", data={
4683
- "dry_run": dry_run,
4684
- "total_found": result.get("total_found", 0),
4685
- "orphans_found": result.get("orphans_found", 0),
4686
- "killed": result.get("killed", 0),
4687
- "skipped": result.get("skipped", 0),
4688
- }, next_actions=[
4689
- {"command": "slm reap --force --json", "description": "Kill orphan processes"},
4690
- {"command": "slm reap --all --json", "description": "Kill ALL slm mcp sessions (IDE switch)"},
4691
- {"command": "slm status --json", "description": "Check status"},
4692
- ])
4693
- return
4694
-
4695
- total = result.get("total_found", 0)
4696
- orphans = result.get("orphans_found", 0)
4697
- killed = result.get("killed", 0)
4698
- skipped = result.get("skipped", 0)
4699
-
4700
- if dry_run:
4701
- print(f"Process reaper (dry run)")
4702
- else:
4703
- print(f"Process reaper")
4704
- print(f" Total SLM processes: {total}")
4705
- print(f" Orphans found: {orphans}")
4706
- print(f" Killed: {killed}")
4707
- print(f" Skipped: {skipped}")
4708
- if dry_run and orphans > 0:
4709
- print("\n Use --force to kill orphaned processes.")
4710
- print(" Use --all to kill ALL slm mcp sessions (after IDE switch).")