superlocalmemory 3.6.23 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +271 -71
  3. package/bin/slm-npm +43 -89
  4. package/ide/configs/antigravity-mcp.json +2 -2
  5. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  6. package/ide/configs/claude-desktop-mcp.json +2 -2
  7. package/ide/configs/windsurf-mcp.json +2 -2
  8. package/ide/hooks/context-hook.js +6 -2
  9. package/ide/hooks/post-recall-hook.js +7 -3
  10. package/ide/hooks/tool-event-hook.sh +2 -1
  11. package/package.json +18 -10
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/_GENERATED.md +1 -1
  14. package/plugin/agents/slm-memory-advisor.md +1 -1
  15. package/plugin/requirements.txt +1 -1
  16. package/plugin/skills/slm-session/SKILL.md +1 -1
  17. package/plugin-src/rules/AGENTS.md +1 -1
  18. package/pyproject.toml +40 -8
  19. package/scripts/postinstall-interactive.js +17 -94
  20. package/scripts/postinstall.js +185 -258
  21. package/scripts/preuninstall.js +9 -50
  22. package/src/superlocalmemory/__init__.py +2 -2
  23. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  24. package/src/superlocalmemory/attribution/signer.py +34 -19
  25. package/src/superlocalmemory/attribution/watermark.py +1 -1
  26. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  27. package/src/superlocalmemory/cli/commands.py +453 -191
  28. package/src/superlocalmemory/cli/context_commands.py +5 -4
  29. package/src/superlocalmemory/cli/daemon.py +282 -187
  30. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  31. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  32. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  33. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  34. package/src/superlocalmemory/cli/main.py +128 -31
  35. package/src/superlocalmemory/cli/pending_store.py +54 -38
  36. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  37. package/src/superlocalmemory/cli/service_installer.py +57 -52
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  39. package/src/superlocalmemory/cli/version_banner.py +2 -1
  40. package/src/superlocalmemory/code_graph/config.py +3 -1
  41. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  42. package/src/superlocalmemory/core/config.py +65 -20
  43. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  44. package/src/superlocalmemory/core/context_cache.py +56 -8
  45. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  46. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  47. package/src/superlocalmemory/core/embeddings.py +54 -18
  48. package/src/superlocalmemory/core/engine.py +150 -104
  49. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  50. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  51. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  52. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  53. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  54. package/src/superlocalmemory/core/health_monitor.py +4 -2
  55. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  56. package/src/superlocalmemory/core/injection.py +69 -18
  57. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  58. package/src/superlocalmemory/core/maintenance.py +1 -1
  59. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  60. package/src/superlocalmemory/core/mutations.py +143 -0
  61. package/src/superlocalmemory/core/platform_utils.py +7 -4
  62. package/src/superlocalmemory/core/ram_lock.py +16 -5
  63. package/src/superlocalmemory/core/rate_limit.py +1 -1
  64. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  65. package/src/superlocalmemory/core/recall_worker.py +76 -59
  66. package/src/superlocalmemory/core/registry.py +1 -1
  67. package/src/superlocalmemory/core/scale_engine.py +293 -0
  68. package/src/superlocalmemory/core/score_contract.py +62 -0
  69. package/src/superlocalmemory/core/security_primitives.py +3 -1
  70. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  72. package/src/superlocalmemory/core/tier_manager.py +32 -20
  73. package/src/superlocalmemory/core/worker_pool.py +13 -4
  74. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  75. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  76. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  77. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  79. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  80. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  81. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  82. package/src/superlocalmemory/encoding/foresight.py +2 -0
  83. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  84. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  85. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  86. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  87. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  88. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  89. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  90. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  91. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  92. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  93. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  94. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  95. package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
  96. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  97. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  98. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  99. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  100. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  101. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  102. package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
  103. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  104. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  105. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  106. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  107. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  108. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  109. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  110. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  111. package/src/superlocalmemory/infra/backup.py +7 -5
  112. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  113. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  114. package/src/superlocalmemory/infra/data_root.py +199 -0
  115. package/src/superlocalmemory/infra/event_bus.py +3 -1
  116. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  117. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  118. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  119. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  120. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  121. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  122. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  123. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  124. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  125. package/src/superlocalmemory/learning/ensemble.py +11 -0
  126. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  127. package/src/superlocalmemory/learning/feedback.py +1 -1
  128. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  129. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  130. package/src/superlocalmemory/learning/ranker.py +4 -1
  131. package/src/superlocalmemory/learning/source_quality.py +1 -1
  132. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  133. package/src/superlocalmemory/llm/backbone.py +1 -1
  134. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  135. package/src/superlocalmemory/math/fisher.py +1 -1
  136. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  137. package/src/superlocalmemory/math/hopfield.py +1 -1
  138. package/src/superlocalmemory/math/langevin.py +1 -1
  139. package/src/superlocalmemory/math/polar_quant.py +3 -4
  140. package/src/superlocalmemory/math/qjl.py +1 -1
  141. package/src/superlocalmemory/math/sheaf.py +1 -1
  142. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  143. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  144. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  145. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  146. package/src/superlocalmemory/mcp/server.py +39 -13
  147. package/src/superlocalmemory/mcp/shared.py +69 -3
  148. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  149. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  150. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  151. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  152. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  153. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  154. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  155. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  156. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  157. package/src/superlocalmemory/mesh/broker.py +2 -0
  158. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  159. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  160. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  161. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  162. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  163. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  164. package/src/superlocalmemory/optimize/config/store.py +6 -4
  165. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  166. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  167. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  168. package/src/superlocalmemory/optimize/storage/db.py +12 -12
  169. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  170. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  171. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  172. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  173. package/src/superlocalmemory/retrieval/engine.py +169 -79
  174. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  175. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  176. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  177. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  178. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  179. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  180. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  181. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  182. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  183. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  184. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  185. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  186. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  187. package/src/superlocalmemory/server/api.py +10 -7
  188. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  189. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  190. package/src/superlocalmemory/server/route_mutations.py +84 -0
  191. package/src/superlocalmemory/server/routes/agents.py +8 -6
  192. package/src/superlocalmemory/server/routes/brain.py +14 -12
  193. package/src/superlocalmemory/server/routes/chat.py +29 -12
  194. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  195. package/src/superlocalmemory/server/routes/helpers.py +8 -63
  196. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  197. package/src/superlocalmemory/server/routes/memories.py +104 -43
  198. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  199. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  200. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  201. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  202. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  203. package/src/superlocalmemory/server/security_middleware.py +1 -1
  204. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  205. package/src/superlocalmemory/server/write_identity.py +147 -0
  206. package/src/superlocalmemory/storage/access_log.py +4 -3
  207. package/src/superlocalmemory/storage/database.py +118 -25
  208. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  209. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  210. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  211. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  212. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  213. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  214. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  215. package/src/superlocalmemory/storage/models.py +16 -0
  216. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  217. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  218. package/src/superlocalmemory/ui/favicon.svg +5 -0
  219. package/src/superlocalmemory/ui/index.html +1 -0
  220. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  221. package/src/superlocalmemory/ui/js/core.js +49 -8
  222. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  223. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  224. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  225. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  226. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  227. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  228. package/src/superlocalmemory/ui/js/settings.js +4 -2
  229. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  230. package/bin/slm +0 -59
  231. package/bin/slm.bat +0 -77
  232. package/bin/slm.cmd +0 -5
  233. package/ide/integrations/langchain/README.md +0 -106
  234. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  235. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  236. package/ide/integrations/langchain/pyproject.toml +0 -38
  237. package/ide/integrations/langchain/tests/__init__.py +0 -3
  238. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  239. package/ide/integrations/langchain/tests/test_security.py +0 -117
  240. package/ide/integrations/llamaindex/README.md +0 -81
  241. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  242. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  243. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  244. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  245. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  246. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  247. package/plugin-src/.mcp.json +0 -12
  248. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  249. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  250. package/plugin-src/hooks/.gitkeep +0 -0
  251. package/plugin-src/hooks/hooks.json +0 -23
  252. package/plugin-src/manifest.json +0 -25
  253. package/plugin-src/requirements.txt +0 -1
  254. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  255. package/plugin-src/scripts/ensure-venv.bat +0 -122
  256. package/plugin-src/scripts/ensure-venv.sh +0 -105
  257. package/plugin-src/scripts/slm-launch +0 -15
  258. package/plugin-src/scripts/slm-launch.bat +0 -17
  259. package/plugin-src/settings.json +0 -16
  260. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  261. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  262. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  263. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  264. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  265. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  266. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  267. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  268. package/scripts/_savings_math.py +0 -270
  269. package/scripts/build-dmg.sh +0 -417
  270. package/scripts/build-plugin.js +0 -742
  271. package/scripts/build-slm-hook.ps1 +0 -40
  272. package/scripts/build-slm-hook.sh +0 -45
  273. package/scripts/build_entry.py +0 -452
  274. package/scripts/ci/stage5b_gate.sh +0 -50
  275. package/scripts/dogfood_savings.py +0 -490
  276. package/scripts/generate-thumbnails.py +0 -218
  277. package/scripts/install-skills.ps1 +0 -4
  278. package/scripts/install-skills.sh +0 -5
  279. package/scripts/install.ps1 +0 -701
  280. package/scripts/install.sh +0 -1015
  281. package/scripts/postinstall_binary.js +0 -287
  282. package/scripts/prepack.js +0 -33
  283. package/scripts/release_manifest.py +0 -273
  284. package/scripts/slm-hook.spec +0 -56
  285. package/scripts/start-dashboard.ps1 +0 -52
  286. package/scripts/start-dashboard.sh +0 -41
  287. package/scripts/sync-wiki.ps1 +0 -127
  288. package/scripts/sync-wiki.sh +0 -82
  289. package/scripts/test-dmg.sh +0 -161
  290. package/scripts/test-npm-package.ps1 +0 -252
  291. package/scripts/test-npm-package.sh +0 -207
  292. package/scripts/verify-install.ps1 +0 -294
  293. package/scripts/verify-install.sh +0 -266
  294. package/scripts/verify-v27.ps1 +0 -301
  295. package/scripts/verify-v27.sh +0 -233
  296. package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
  297. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  298. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  299. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  300. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  301. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -0,0 +1,103 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """CLI for versioned evidence export, verification, import, and rebuild."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from argparse import Namespace
10
+ from pathlib import Path
11
+
12
+
13
+ def _database():
14
+ from superlocalmemory.core.config import SLMConfig
15
+ from superlocalmemory.storage.database import DatabaseManager
16
+
17
+ config = SLMConfig.load()
18
+ return DatabaseManager(config.db_path)
19
+
20
+
21
+ def _emit(value: dict, *, as_json: bool) -> None:
22
+ if as_json:
23
+ print(json.dumps(value, sort_keys=True))
24
+ return
25
+ for key, item in value.items():
26
+ print(f"{key}: {item}")
27
+
28
+
29
+ def cmd_evidence(args: Namespace) -> None:
30
+ """Dispatch ``slm evidence`` without implicit destructive behavior."""
31
+ from superlocalmemory.core.evidence_bundle import (
32
+ export_evidence_bundle,
33
+ import_evidence_bundle,
34
+ rebuild_derived_state,
35
+ verify_evidence_bundle,
36
+ )
37
+
38
+ action = getattr(args, "evidence_command", None)
39
+ as_json = bool(getattr(args, "json", False))
40
+ profile = str(getattr(args, "profile", "default") or "default")
41
+
42
+ if action == "verify":
43
+ report = verify_evidence_bundle(args.bundle)
44
+ _emit({
45
+ "valid": report.valid,
46
+ "bundle_id": report.bundle_id,
47
+ "counts": report.counts,
48
+ "errors": list(report.errors),
49
+ "warnings": list(report.warnings),
50
+ }, as_json=as_json)
51
+ if not report.valid:
52
+ raise SystemExit(1)
53
+ return
54
+
55
+ if action == "export":
56
+ db = _database()
57
+ if not Path(db.db_path).is_file():
58
+ raise SystemExit(f"memory database not found: {db.db_path}")
59
+ manifest = export_evidence_bundle(db, profile, args.destination)
60
+ _emit({
61
+ "bundle_id": manifest["bundle_id"],
62
+ "profile_id": profile,
63
+ "destination": str(Path(args.destination).resolve()),
64
+ "unresolved_source_links": manifest["unresolved_source_links"],
65
+ }, as_json=as_json)
66
+ return
67
+
68
+ if action == "import":
69
+ if not bool(getattr(args, "execute", False)):
70
+ print(
71
+ "Dry run only. Verify the bundle, then re-run with --execute; "
72
+ "replacement also requires --rollback-dir."
73
+ )
74
+ return
75
+ report = import_evidence_bundle(
76
+ _database(),
77
+ args.bundle,
78
+ target_profile_id=profile,
79
+ replace=bool(getattr(args, "replace", False)),
80
+ rollback_dir=getattr(args, "rollback_dir", None),
81
+ )
82
+ _emit({
83
+ "valid": report.valid,
84
+ "bundle_id": report.bundle_id,
85
+ "profile_id": profile,
86
+ "counts": report.counts,
87
+ }, as_json=as_json)
88
+ return
89
+
90
+ if action == "rebuild":
91
+ if not bool(getattr(args, "execute", False)):
92
+ print(
93
+ "Dry run only. Re-run with --execute to rebuild derived lexical state."
94
+ )
95
+ return
96
+ result = rebuild_derived_state(_database(), profile)
97
+ _emit({"profile_id": profile, **result}, as_json=as_json)
98
+ return
99
+
100
+ raise SystemExit("choose an evidence subcommand: export, verify, import, or rebuild")
101
+
102
+
103
+ __all__ = ["cmd_evidence"]
@@ -24,10 +24,14 @@ from argparse import Namespace
24
24
  from datetime import datetime, timezone
25
25
  from pathlib import Path
26
26
 
27
+ from superlocalmemory.infra.data_root import DynamicStatePath
28
+
27
29
  logger = logging.getLogger(__name__)
28
30
 
29
- MEMORY_DIR = Path.home() / ".superlocalmemory"
30
- MEMORY_DB = MEMORY_DIR / "memory.db"
31
+ # Backward-compatible public paths that resolve at call time instead of
32
+ # freezing Path.home() during module import.
33
+ MEMORY_DIR = DynamicStatePath()
34
+ MEMORY_DB = DynamicStatePath("memory.db")
31
35
 
32
36
 
33
37
  def cmd_ingest(args: Namespace) -> None:
@@ -278,7 +282,7 @@ def _write_tool_events(events: list[dict]) -> int:
278
282
  v3.4.10: Preserves input_summary, output_summary, and project_path
279
283
  from enriched sources (ECC observations, enriched hook).
280
284
  """
281
- db_path = MEMORY_DB
285
+ db_path = Path(MEMORY_DB)
282
286
  if not db_path.exists():
283
287
  return 0
284
288
 
@@ -28,18 +28,16 @@ from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
28
28
 
29
29
  _HELP_EPILOG = """\
30
30
  operating modes:
31
- Mode A Local Guardian — Zero cloud, zero LLM. All processing stays on
32
- your machine. Full EU AI Act compliance. Best for privacy-first
33
- use, air-gapped systems, and regulated environments.
34
- Retrieval score: 74.8% on LoCoMo benchmark.
31
+ Mode A Local Guardian — No model-provider call in the core memory path.
32
+ Optional integrations, backup, and downloads have separate network
33
+ behavior; assess the complete deployment for privacy requirements.
35
34
 
36
35
  Mode B Smart Local — Uses a local Ollama LLM for summarization and
37
- enrichment. Data never leaves your network. EU AI Act compliant.
38
- Requires: ollama running locally with a model pulled.
36
+ enrichment. The Ollama endpoint and enabled integrations determine
37
+ the data path. Requires: ollama running with a model pulled.
39
38
 
40
39
  Mode C Full Power — Uses a cloud LLM (OpenAI, Anthropic, etc.) for
41
- maximum accuracy. Best retrieval quality, agentic multi-hop.
42
- Retrieval score: 87.7% on LoCoMo benchmark.
40
+ provider-assisted enrichment and retrieval behavior.
43
41
 
44
42
  quick start:
45
43
  slm setup Interactive first-time setup
@@ -70,6 +68,34 @@ documentation:
70
68
  """
71
69
 
72
70
 
71
+ _NO_DAEMON_COMMANDS = {
72
+ "setup", "mode", "provider", "connect", "migrate", "mcp", "warmup", "hooks", "codex",
73
+ "config", "evolve", "db",
74
+ # v3.4.22 escape hatches — never auto-start the daemon on these.
75
+ "disable", "enable", "clear-cache", "reconfigure", "benchmark",
76
+ "rotate-token",
77
+ "evidence",
78
+ "diagnostics",
79
+ # LLD-06 — agents launched through wrap start the daemon on demand.
80
+ "wrap",
81
+ # V3.6 Optimize commands that are config read/write only.
82
+ "optimize", "cache", "compress", "help-optimize",
83
+ # Lifecycle orchestration must run before any global auto-start hook.
84
+ "serve", "restart",
85
+ }
86
+
87
+
88
+ def _command_requires_daemon(args: argparse.Namespace) -> bool:
89
+ """Return whether global dispatch should ensure the daemon first.
90
+
91
+ All ``serve`` actions and ``restart`` own daemon inspection or mutation;
92
+ global dispatch must not race or duplicate those handlers.
93
+ """
94
+ if args.command in _NO_DAEMON_COMMANDS:
95
+ return False
96
+ return True
97
+
98
+
73
99
  def main() -> None:
74
100
  """Parse CLI arguments and dispatch to command handlers."""
75
101
  # Fast path: hook invocations bypass argparse entirely (stdlib only, ~30ms)
@@ -78,11 +104,20 @@ def main() -> None:
78
104
  handle_hook(sys.argv[2])
79
105
  return
80
106
 
107
+ # Metadata inspection is activation-free. Package managers, users, and CI
108
+ # must be able to inspect a newly installed executable without creating an
109
+ # SLM data root, config, version marker, daemon, hooks, or models.
110
+ _is_metadata_cmd = (
111
+ len(sys.argv) == 1
112
+ or any(arg in {"-h", "--help"} for arg in sys.argv[1:])
113
+ or (len(sys.argv) == 2 and sys.argv[1] in {"-v", "--version"})
114
+ )
115
+
81
116
  # WP-07: lazy first-run init — runs after hook/mcp fast-paths so stdout
82
117
  # is never polluted on those paths (CRIT-3, MCP JSON-RPC purity).
83
118
  # Guarded: any failure must not crash the CLI (AC4).
84
119
  _is_mcp_cmd = len(sys.argv) >= 2 and sys.argv[1] == "mcp"
85
- if not _is_mcp_cmd:
120
+ if not _is_mcp_cmd and not _is_metadata_cmd:
86
121
  try:
87
122
  from superlocalmemory.cli._lazy_init import _ensure_initialized
88
123
  _ensure_initialized()
@@ -102,7 +137,7 @@ def main() -> None:
102
137
 
103
138
  # One-time post-upgrade banner — silent for fresh installs and
104
139
  # same-version runs. Guarded against I/O errors internally.
105
- if not _is_mcp_stdio:
140
+ if not _is_mcp_stdio and not _is_metadata_cmd:
106
141
  from superlocalmemory.cli.version_banner import check_and_emit_upgrade_banner
107
142
  if check_and_emit_upgrade_banner(_ver):
108
143
  # First post-upgrade invocation: apply the data-dir migration if
@@ -110,7 +145,6 @@ def main() -> None:
110
145
  # we defer — the next daemon start picks it up.
111
146
  try:
112
147
  import logging as _logging
113
- from pathlib import Path as _P
114
148
  from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
115
149
  migrate_if_safe as _migrate_if_safe,
116
150
  )
@@ -176,8 +210,13 @@ def main() -> None:
176
210
  provider_p.add_argument(
177
211
  "action", nargs="?", choices=["set"], help="Action",
178
212
  )
213
+ provider_p.add_argument(
214
+ "provider", nargs="?",
215
+ choices=["openai", "anthropic", "azure", "ollama", "openrouter"],
216
+ help="Provider to configure; omit to use the interactive selector",
217
+ )
179
218
 
180
- connect_p = sub.add_parser("connect", help="Auto-configure IDE integrations (17+ IDEs)")
219
+ connect_p = sub.add_parser("connect", help="Auto-configure a supported IDE integration")
181
220
  connect_p.add_argument("ide", nargs="?", help="Specific IDE to configure (e.g. cursor, codex, continue)")
182
221
  connect_p.add_argument(
183
222
  "--list", action="store_true", help="List all supported IDEs",
@@ -225,6 +264,17 @@ def main() -> None:
225
264
  "--dry-run", action="store_true",
226
265
  help="Report what would change without applying",
227
266
  )
267
+ db_scale_p = db_sub.add_parser(
268
+ "scale",
269
+ help="Safely stage and promote optional CozoDB/LanceDB projections",
270
+ )
271
+ db_scale_p.add_argument(
272
+ "scale_action",
273
+ choices=("status", "prepare", "verify", "promote", "rollback"),
274
+ help="Lifecycle action. Prepare and verify never mutate active projection paths.",
275
+ )
276
+ db_scale_p.add_argument("--stage-id", help="Stage identifier required by verify/promote")
277
+ db_scale_p.add_argument("--backup-id", help="Backup identifier required by rollback")
228
278
 
229
279
  # -- Memory Operations ---------------------------------------------
230
280
  remember_p = sub.add_parser("remember", help="Store a memory (extracts facts, builds graph)")
@@ -247,7 +297,7 @@ def main() -> None:
247
297
 
248
298
  # v3.6.12 (parity-3): `search` is an alias of `recall` so the CLI has the
249
299
  # same search verb the MCP exposes (handlers dict maps both to cmd_recall).
250
- recall_p = sub.add_parser("recall", aliases=["search"], help="Semantic search with 4-channel retrieval")
300
+ recall_p = sub.add_parser("recall", aliases=["search"], help="Multi-channel memory retrieval")
251
301
  recall_p.add_argument("query", help="Search query")
252
302
  recall_p.add_argument(
253
303
  "--limit", type=int, default=CANONICAL_RECALL_LIMIT,
@@ -256,8 +306,8 @@ def main() -> None:
256
306
  recall_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
257
307
  recall_p.add_argument(
258
308
  "--fast", action="store_true",
259
- help="Skip SpreadingActivation 5th channel for sub-second response. "
260
- "Other 4 channels (semantic, lexical, temporal, structural) still run. "
309
+ help="Skip spreading activation and remote agentic verification for a "
310
+ "latency-bounded response. Other configured retrieval channels still run. "
261
311
  "Use when you need recall before a tool call (e.g. before WebSearch).",
262
312
  )
263
313
  # v3.6.15: shared memory is opt-in. Unset (None) → resolve the configured
@@ -386,7 +436,7 @@ def main() -> None:
386
436
  profile_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
387
437
 
388
438
  # -- Active Memory (V3.1) ------------------------------------------
389
- hooks_p = sub.add_parser("hooks", help="Manage Claude Code hooks for auto memory injection")
439
+ hooks_p = sub.add_parser("hooks", help="Manage additive Claude Code or Codex hooks")
390
440
  hooks_p.add_argument(
391
441
  "action", nargs="?", default="status",
392
442
  choices=["install", "remove", "status"], help="Action (default: status)",
@@ -395,6 +445,18 @@ def main() -> None:
395
445
  "--gate", action="store_true",
396
446
  help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
397
447
  )
448
+ hooks_p.add_argument(
449
+ "--agent", choices=["claude", "codex"], default="claude",
450
+ help="Target agent (default: claude)",
451
+ )
452
+ hooks_p.add_argument(
453
+ "--dry-run", action="store_true",
454
+ help="Show whether the additive change is valid without writing files",
455
+ )
456
+
457
+ codex_p = sub.add_parser("codex", help="Install or inspect explicit Codex SLM add-ons")
458
+ codex_p.add_argument("action", nargs="?", default="status", choices=["install", "remove", "status"])
459
+ codex_p.add_argument("--dry-run", action="store_true", help="Validate without writing user files")
398
460
 
399
461
  ctx_p = sub.add_parser("session-context", help="Print session context (for hooks)")
400
462
  ctx_p.add_argument("query", nargs="?", default="", help="Optional context query")
@@ -568,6 +630,55 @@ def main() -> None:
568
630
  help="Emit JSON result instead of human-readable summary",
569
631
  )
570
632
 
633
+ evidence_p = sub.add_parser(
634
+ "evidence",
635
+ help="Export, verify, import, or rebuild versioned memory evidence",
636
+ )
637
+ evidence_sub = evidence_p.add_subparsers(
638
+ dest="evidence_command", title="evidence subcommands",
639
+ )
640
+ evidence_export = evidence_sub.add_parser(
641
+ "export", help="Write a deterministic checksummed JSONL bundle",
642
+ )
643
+ evidence_export.add_argument("destination")
644
+ evidence_export.add_argument("--profile", default="default")
645
+ evidence_export.add_argument("--json", action="store_true")
646
+ evidence_verify = evidence_sub.add_parser(
647
+ "verify", help="Verify checksums and source reconciliation",
648
+ )
649
+ evidence_verify.add_argument("bundle")
650
+ evidence_verify.add_argument("--json", action="store_true")
651
+ evidence_import = evidence_sub.add_parser(
652
+ "import", help="Import relational truth (dry-run unless --execute)",
653
+ )
654
+ evidence_import.add_argument("bundle")
655
+ evidence_import.add_argument("--profile", default="default")
656
+ evidence_import.add_argument("--replace", action="store_true")
657
+ evidence_import.add_argument("--rollback-dir", default=None)
658
+ evidence_import.add_argument("--execute", action="store_true")
659
+ evidence_import.add_argument("--json", action="store_true")
660
+ evidence_rebuild = evidence_sub.add_parser(
661
+ "rebuild", help="Rebuild derived lexical state (dry-run unless --execute)",
662
+ )
663
+ evidence_rebuild.add_argument("--profile", default="default")
664
+ evidence_rebuild.add_argument("--execute", action="store_true")
665
+ evidence_rebuild.add_argument("--json", action="store_true")
666
+
667
+ diagnostics_p = sub.add_parser(
668
+ "diagnostics",
669
+ help="Export bounded local operational aggregates (manual only)",
670
+ )
671
+ diagnostics_sub = diagnostics_p.add_subparsers(
672
+ dest="diagnostics_command", title="diagnostics subcommands",
673
+ )
674
+ diagnostics_export = diagnostics_sub.add_parser(
675
+ "export", help="Write a deterministic content-free JSON report",
676
+ )
677
+ diagnostics_export.add_argument("destination")
678
+ diagnostics_export.add_argument(
679
+ "--json", action="store_true", help="Output structured JSON",
680
+ )
681
+
571
682
  # S-M07: install-token rotation.
572
683
  sub.add_parser(
573
684
  "rotate-token",
@@ -676,21 +787,7 @@ def main() -> None:
676
787
  # V3.4.4: Auto-start daemon for all commands that need it.
677
788
  # SLM is always-on — close laptop, reboot, crash: daemon auto-recovers.
678
789
  # Cross-platform: macOS + Windows + Linux.
679
- _NO_DAEMON_COMMANDS = {
680
- "setup", "mode", "provider", "connect", "migrate", "mcp", "warmup",
681
- "config", "evolve", "db",
682
- # v3.4.22 escape hatches — never auto-start the daemon on these.
683
- "disable", "enable", "clear-cache", "reconfigure", "benchmark",
684
- "rotate-token",
685
- # LLD-06 — `slm wrap` may launch the agent binary directly without
686
- # needing the daemon running (the agent will start the daemon on
687
- # first LLM call, or the wrap command can be --dry-run).
688
- "wrap",
689
- # V3.6 Optimize commands that are config read/write only (no daemon needed)
690
- "optimize", "cache", "compress", "help-optimize",
691
- # NOTE: "proxy" NOT here — proxy needs daemon running
692
- }
693
- if args.command not in _NO_DAEMON_COMMANDS:
790
+ if _command_requires_daemon(args):
694
791
  try:
695
792
  from superlocalmemory.cli.daemon import ensure_daemon
696
793
  ensure_daemon() # Starts daemon if not running; no-op if already up
@@ -18,27 +18,27 @@ Uses a separate `pending.db` file — never touches memory.db directly.
18
18
  Stdlib only — no SLM imports (must be fast).
19
19
 
20
20
  Part of Qualixar | Author: Varun Pratap Bhardwaj
21
- License: Elastic-2.0
21
+ License: AGPL-3.0-or-later
22
22
  """
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
26
  import json
27
- import os
28
27
  import sqlite3
29
28
  import time
30
29
  from pathlib import Path
31
30
 
32
31
 
33
32
  def _default_dir() -> Path:
34
- """Honor SLM_DATA_DIR so tests can isolate via tmp_path."""
35
- return Path(os.environ.get("SLM_DATA_DIR") or Path.home() / ".superlocalmemory")
33
+ """Resolve the pending queue inside the canonical process namespace."""
34
+ from superlocalmemory.infra.data_root import canonical_data_root
35
+ return canonical_data_root()
36
36
 
37
37
 
38
38
  _PENDING_DB = "pending.db"
39
39
  _MAX_RETRIES = 3
40
40
  _STUCK_DAYS = 7
41
- _DEAD_LETTER_DAYS = 30
41
+ _MAX_RETRY_DELAY_SECONDS = 3600
42
42
 
43
43
  _SCHEMA = """
44
44
  CREATE TABLE IF NOT EXISTS pending_memories (
@@ -49,7 +49,8 @@ CREATE TABLE IF NOT EXISTS pending_memories (
49
49
  created_at TEXT NOT NULL,
50
50
  status TEXT NOT NULL DEFAULT 'pending',
51
51
  error TEXT DEFAULT NULL,
52
- retry_count INTEGER DEFAULT 0
52
+ retry_count INTEGER DEFAULT 0,
53
+ next_retry_at REAL DEFAULT 0
53
54
  );
54
55
  """
55
56
 
@@ -62,6 +63,24 @@ def _get_db(base_dir: Path | None = None) -> sqlite3.Connection:
62
63
  conn = sqlite3.connect(str(db_path), timeout=5)
63
64
  conn.execute("PRAGMA journal_mode=WAL")
64
65
  conn.execute(_SCHEMA)
66
+ columns = {
67
+ row[1]
68
+ for row in conn.execute("PRAGMA table_info(pending_memories)").fetchall()
69
+ }
70
+ if "next_retry_at" not in columns:
71
+ conn.execute(
72
+ "ALTER TABLE pending_memories ADD COLUMN "
73
+ "next_retry_at REAL DEFAULT 0"
74
+ )
75
+ conn.commit()
76
+ # Pre-V3.7 rows were terminally hidden after three failures. Restore them
77
+ # to the retry queue; M018 makes replay idempotent and no raw evidence may
78
+ # remain stranded solely because an older version exhausted its counter.
79
+ conn.execute(
80
+ "UPDATE pending_memories SET status='pending', next_retry_at=0 "
81
+ "WHERE status='failed'"
82
+ )
83
+ conn.commit()
65
84
  return conn
66
85
 
67
86
 
@@ -96,8 +115,9 @@ def get_pending(base_dir: Path | None = None, limit: int = 50) -> list[dict]:
96
115
  rows = conn.execute(
97
116
  "SELECT id, content, tags, metadata, created_at, retry_count "
98
117
  "FROM pending_memories WHERE status = 'pending' "
118
+ "AND COALESCE(next_retry_at, 0) <= ? "
99
119
  "ORDER BY id ASC LIMIT ?",
100
- (limit,),
120
+ (time.time(), limit),
101
121
  ).fetchall()
102
122
  return [
103
123
  {"id": r[0], "content": r[1], "tags": r[2], "metadata": r[3],
@@ -124,20 +144,29 @@ def mark_done(row_id: int, base_dir: Path | None = None) -> None:
124
144
  def mark_failed(row_id: int, error: str, base_dir: Path | None = None) -> None:
125
145
  """Mark a pending memory as failed with error message.
126
146
 
127
- v3.4.38: Now retry-aware. If retry_count < _MAX_RETRIES, keeps status as
128
- 'pending' so the materializer will retry on next iteration. Only marks
129
- permanently failed after _MAX_RETRIES (3) attempts. The previous behavior
130
- permanently lost 18 memories between April 15-26, 2026 to transient errors.
147
+ Unprocessed evidence is never deleted or terminally hidden. Failures stay
148
+ pending with bounded exponential backoff; M018 idempotency makes repeated
149
+ replay safe once the canonical operation has been created.
131
150
  """
132
151
  conn = _get_db(base_dir)
133
152
  try:
134
- # Increment retry count and conditionally update status
153
+ row = conn.execute(
154
+ "SELECT retry_count FROM pending_memories WHERE id=?",
155
+ (row_id,),
156
+ ).fetchone()
157
+ if row is None:
158
+ return
159
+ next_count = int(row[0] or 0) + 1
160
+ delay = 0 if next_count == 1 else min(
161
+ 2 ** min(next_count - 1, 12),
162
+ _MAX_RETRY_DELAY_SECONDS,
163
+ )
135
164
  conn.execute(
136
165
  "UPDATE pending_memories SET error = ?, "
137
166
  "retry_count = retry_count + 1, "
138
- "status = CASE WHEN retry_count + 1 >= ? THEN 'failed' ELSE 'pending' END "
167
+ "status = 'pending', next_retry_at = ? "
139
168
  "WHERE id = ?",
140
- (error, _MAX_RETRIES, row_id),
169
+ (error, time.time() + delay, row_id),
141
170
  )
142
171
  conn.commit()
143
172
  finally:
@@ -179,11 +208,8 @@ def cleanup_done(days: int = 7, base_dir: Path | None = None) -> int:
179
208
  def cleanup_stale(base_dir: Path | None = None) -> dict[str, int]:
180
209
  """Sweep stale rows from pending.db. Runs periodically from the daemon.
181
210
 
182
- Removes:
183
- - `done` rows older than 7 days (already processed)
184
- - `failed` rows that exceeded max retries (moved to dead-letter via deletion)
185
- - `pending` rows stuck more than 7 days (test pollution, crashed workers)
186
- - Everything older than 30 days regardless of status (hard cap)
211
+ Deletes only completed receipts. Pending and failed raw evidence is retained
212
+ regardless of age so maintenance can never become a data-loss path.
187
213
  """
188
214
  conn = _get_db(base_dir)
189
215
  try:
@@ -192,28 +218,18 @@ def cleanup_stale(base_dir: Path | None = None) -> dict[str, int]:
192
218
  "AND created_at < datetime('now', ?)",
193
219
  (f"-{_STUCK_DAYS} days",),
194
220
  ).rowcount
195
- failed = conn.execute(
196
- "DELETE FROM pending_memories WHERE status = 'failed' "
197
- "AND retry_count >= ?",
198
- (_MAX_RETRIES,),
199
- ).rowcount
200
- stuck = conn.execute(
201
- "DELETE FROM pending_memories WHERE status = 'pending' "
202
- "AND created_at < datetime('now', ?)",
203
- (f"-{_STUCK_DAYS} days",),
204
- ).rowcount
205
- hard_cap = conn.execute(
206
- "DELETE FROM pending_memories "
207
- "WHERE created_at < datetime('now', ?)",
208
- (f"-{_DEAD_LETTER_DAYS} days",),
209
- ).rowcount
221
+ retained = conn.execute(
222
+ "SELECT COUNT(*) FROM pending_memories "
223
+ "WHERE status != 'done'"
224
+ ).fetchone()[0]
210
225
  conn.commit()
211
226
  return {
212
227
  "done": done,
213
- "failed_over_retries": failed,
214
- "stuck_pending": stuck,
215
- "hard_cap_expired": hard_cap,
216
- "total": done + failed + stuck + hard_cap,
228
+ "failed_over_retries": 0,
229
+ "stuck_pending": 0,
230
+ "hard_cap_expired": 0,
231
+ "retained_unprocessed": int(retained),
232
+ "total": done,
217
233
  }
218
234
  finally:
219
235
  conn.close()
@@ -0,0 +1,37 @@
1
+ """CLI surface for the staged Scale Engine lifecycle."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from argparse import Namespace
6
+
7
+
8
+ def cmd_db_scale(args: Namespace) -> int:
9
+ from superlocalmemory.core.config import SLMConfig
10
+ from superlocalmemory.core.scale_engine import ScaleEngineError, ScaleEngineManager
11
+
12
+ manager = ScaleEngineManager(SLMConfig.load())
13
+ action = args.scale_action
14
+ try:
15
+ if action == "status":
16
+ result = manager.status()
17
+ elif action == "prepare":
18
+ result = manager.prepare()
19
+ elif action == "verify":
20
+ if not args.stage_id:
21
+ raise ScaleEngineError("verify requires --stage-id (see `slm db scale status`)")
22
+ result = manager.verify(args.stage_id)
23
+ elif action == "promote":
24
+ if not args.stage_id:
25
+ raise ScaleEngineError("promote requires --stage-id (see `slm db scale status`)")
26
+ result = manager.promote(args.stage_id)
27
+ elif action == "rollback":
28
+ if not args.backup_id:
29
+ raise ScaleEngineError("rollback requires --backup-id (see `slm db scale status`)")
30
+ result = manager.rollback(args.backup_id)
31
+ else:
32
+ raise ScaleEngineError(f"unknown Scale Engine action: {action}")
33
+ except ScaleEngineError as exc:
34
+ print(f"Scale Engine: {exc}")
35
+ return 1
36
+ print(json.dumps(result, indent=2, sort_keys=True))
37
+ return 0