superlocalmemory 3.6.22 → 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 (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -0,0 +1,327 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Bounded, content-free operational diagnostics stored on this machine.
5
+
6
+ The store accepts only enumerated operations and normalized client families.
7
+ It persists daily counters, latency buckets, coarse error classes, and client
8
+ family transitions. Export is a deliberate local file operation; this module
9
+ has no automatic reporting mechanism.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import math
16
+ import os
17
+ import re
18
+ import sqlite3
19
+ import tempfile
20
+ from collections.abc import Callable, Iterable
21
+ from datetime import UTC, datetime, timedelta
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from superlocalmemory.infra.data_root import state_path
26
+
27
+ SCHEMA_VERSION = 1
28
+ RETENTION_DAYS = 31
29
+ MAX_COUNTER = 2**63 - 1
30
+
31
+ _OPERATIONS = frozenset(
32
+ {
33
+ "activation",
34
+ "remember",
35
+ "recall",
36
+ "observe",
37
+ "session_open",
38
+ "session_close",
39
+ }
40
+ )
41
+ _CLIENT_FAMILIES = (
42
+ "claude",
43
+ "codex",
44
+ "cursor",
45
+ "windsurf",
46
+ "copilot",
47
+ "vscode",
48
+ "jetbrains",
49
+ "dashboard",
50
+ "hook",
51
+ "mcp",
52
+ "api",
53
+ "cli",
54
+ )
55
+ _LATENCY_BOUNDS_MS = (5, 25, 100, 500, 2_000, 10_000, 60_000)
56
+ _ERROR_CLASSES = frozenset(
57
+ {"timeout", "authorization", "validation", "unavailable", "internal"}
58
+ )
59
+
60
+
61
+ def normalize_client(value: str | None) -> str:
62
+ """Collapse an arbitrary actor label to a fixed non-identifying family."""
63
+ tokens = frozenset(re.findall(r"[a-z0-9]+", str(value or "").lower()))
64
+ for family in _CLIENT_FAMILIES:
65
+ if family in tokens:
66
+ return family
67
+ return "other"
68
+
69
+
70
+ def classify_error(error: BaseException | None) -> str | None:
71
+ """Map an exception type to a fixed class without inspecting its message."""
72
+ if error is None:
73
+ return None
74
+ if isinstance(error, TimeoutError):
75
+ return "timeout"
76
+ if isinstance(error, PermissionError):
77
+ return "authorization"
78
+ if isinstance(error, (TypeError, ValueError)):
79
+ return "validation"
80
+ if isinstance(error, (ConnectionError, FileNotFoundError)):
81
+ return "unavailable"
82
+ return "internal"
83
+
84
+
85
+ def _latency_bucket(duration_ms: float | int | None) -> str | None:
86
+ if duration_ms is None:
87
+ return None
88
+ try:
89
+ value = float(duration_ms)
90
+ except (TypeError, ValueError, OverflowError):
91
+ return "invalid"
92
+ if not math.isfinite(value) or value < 0:
93
+ return "invalid"
94
+ for bound in _LATENCY_BOUNDS_MS:
95
+ if value <= bound:
96
+ return f"le_{bound}"
97
+ return "gt_60000"
98
+
99
+
100
+ class LocalDiagnostics:
101
+ """SQLite-backed bounded aggregate diagnostics.
102
+
103
+ The schema intentionally has no free-text value column. Every persisted
104
+ dimension comes from a finite allow-list, so caller payloads cannot enter
105
+ the database or an export by accident.
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ db_file: str | Path,
111
+ *,
112
+ clock: Callable[[], datetime] | None = None,
113
+ retention_days: int = RETENTION_DAYS,
114
+ ) -> None:
115
+ if retention_days < 1 or retention_days > RETENTION_DAYS:
116
+ raise ValueError(f"retention_days must be between 1 and {RETENTION_DAYS}")
117
+ self._db_file = Path(db_file)
118
+ self._clock = clock or (lambda: datetime.now(UTC))
119
+ self._retention_days = retention_days
120
+
121
+ def _connect(self) -> sqlite3.Connection:
122
+ self._db_file.parent.mkdir(parents=True, exist_ok=True)
123
+ if os.name == "posix":
124
+ os.chmod(self._db_file.parent, 0o700)
125
+ conn = sqlite3.connect(str(self._db_file), timeout=5.0)
126
+ conn.execute(
127
+ "CREATE TABLE IF NOT EXISTS diagnostic_aggregates ("
128
+ "day TEXT NOT NULL, group_name TEXT NOT NULL, "
129
+ "dimension TEXT NOT NULL, bucket TEXT NOT NULL, "
130
+ "value INTEGER NOT NULL CHECK(value >= 0), "
131
+ "PRIMARY KEY(day, group_name, dimension, bucket))"
132
+ )
133
+ conn.commit()
134
+ if os.name == "posix":
135
+ os.chmod(self._db_file, 0o600)
136
+ return conn
137
+
138
+ def _today(self) -> datetime:
139
+ current = self._clock()
140
+ if current.tzinfo is None:
141
+ current = current.replace(tzinfo=UTC)
142
+ return current.astimezone(UTC)
143
+
144
+ @staticmethod
145
+ def _increment(
146
+ conn: sqlite3.Connection,
147
+ day: str,
148
+ group_name: str,
149
+ dimension: str,
150
+ bucket: str,
151
+ ) -> None:
152
+ conn.execute(
153
+ "INSERT INTO diagnostic_aggregates "
154
+ "(day, group_name, dimension, bucket, value) VALUES (?,?,?,?,1) "
155
+ "ON CONFLICT(day, group_name, dimension, bucket) DO UPDATE SET "
156
+ "value=MIN(diagnostic_aggregates.value + 1, ?)",
157
+ (day, group_name, dimension, bucket, MAX_COUNTER),
158
+ )
159
+
160
+ def record(
161
+ self,
162
+ operation: str,
163
+ *,
164
+ client: str | None = None,
165
+ duration_ms: float | int | None = None,
166
+ error: BaseException | None = None,
167
+ source_clients: Iterable[str] = (),
168
+ ) -> None:
169
+ """Record one operation using only fixed aggregate dimensions."""
170
+ if operation not in _OPERATIONS:
171
+ raise ValueError(f"unsupported diagnostic operation: {operation}")
172
+ current = self._today()
173
+ day = current.date().isoformat()
174
+ cutoff = (current.date() - timedelta(days=self._retention_days - 1)).isoformat()
175
+ client_family = normalize_client(client)
176
+ status = "error" if error is not None else "success"
177
+ duration_bucket = _latency_bucket(duration_ms)
178
+ error_class = classify_error(error)
179
+ transitions = {
180
+ f"{normalize_client(source)}_to_{client_family}"
181
+ for source in source_clients
182
+ if normalize_client(source) != client_family
183
+ }
184
+
185
+ conn = self._connect()
186
+ try:
187
+ conn.execute("BEGIN IMMEDIATE")
188
+ conn.execute("DELETE FROM diagnostic_aggregates WHERE day < ?", (cutoff,))
189
+ self._increment(conn, day, "operations", operation, status)
190
+ self._increment(conn, day, "clients", client_family, operation)
191
+ if duration_bucket is not None:
192
+ self._increment(conn, day, "latency_ms", operation, duration_bucket)
193
+ if error_class is not None:
194
+ self._increment(conn, day, "errors", operation, error_class)
195
+ if operation == "activation":
196
+ self._increment(conn, day, "activations", client_family, "count")
197
+ for transition in sorted(transitions):
198
+ self._increment(conn, day, "handoffs", transition, "count")
199
+ conn.commit()
200
+ except BaseException:
201
+ conn.rollback()
202
+ raise
203
+ finally:
204
+ conn.close()
205
+
206
+ def export_payload(self) -> dict[str, Any]:
207
+ """Return a deterministic, sanitized aggregate document."""
208
+ days: dict[str, dict[str, Any]] = {}
209
+ if self._db_file.is_file():
210
+ conn = self._connect()
211
+ try:
212
+ rows = conn.execute(
213
+ "SELECT day, group_name, dimension, bucket, value "
214
+ "FROM diagnostic_aggregates "
215
+ "ORDER BY day, group_name, dimension, bucket"
216
+ ).fetchall()
217
+ finally:
218
+ conn.close()
219
+ for day, group_name, dimension, bucket, value in rows:
220
+ entry = days.setdefault(str(day), {"day": str(day)})
221
+ group = entry.setdefault(str(group_name), {})
222
+ dimension_values = group.setdefault(str(dimension), {})
223
+ dimension_values[str(bucket)] = int(value)
224
+ return {
225
+ "schema_version": SCHEMA_VERSION,
226
+ "retention_days": self._retention_days,
227
+ "privacy": {
228
+ "local_only": True,
229
+ "reporting": "manual_export_only",
230
+ "content_values": False,
231
+ "query_values": False,
232
+ "fact_identifiers": False,
233
+ "filesystem_locations": False,
234
+ "identity_values": False,
235
+ },
236
+ "days": [days[key] for key in sorted(days)],
237
+ }
238
+
239
+ def export_json(self, destination: str | Path) -> dict[str, Any]:
240
+ """Atomically write a deterministic manual export with mode 0600."""
241
+ payload = self.export_payload()
242
+ output = Path(destination)
243
+ output.parent.mkdir(parents=True, exist_ok=True)
244
+ data = (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode("utf-8")
245
+ temporary_name = ""
246
+ try:
247
+ with tempfile.NamedTemporaryFile(
248
+ mode="wb", dir=output.parent, prefix=f".{output.name}.", delete=False,
249
+ ) as temporary:
250
+ temporary_name = temporary.name
251
+ os.chmod(temporary_name, 0o600)
252
+ temporary.write(data)
253
+ temporary.flush()
254
+ os.fsync(temporary.fileno())
255
+ os.replace(temporary_name, output)
256
+ if os.name == "posix":
257
+ os.chmod(output, 0o600)
258
+ finally:
259
+ if temporary_name:
260
+ Path(temporary_name).unlink(missing_ok=True)
261
+ return payload
262
+
263
+
264
+ def default_diagnostics() -> LocalDiagnostics:
265
+ """Return the diagnostics store for the active local state root."""
266
+ return LocalDiagnostics(state_path("diagnostics.db"))
267
+
268
+
269
+ def record_operation(
270
+ operation: str,
271
+ *,
272
+ client: str | None = None,
273
+ duration_ms: float | int | None = None,
274
+ error: BaseException | None = None,
275
+ source_clients: Iterable[str] = (),
276
+ ) -> None:
277
+ """Best-effort hot-path adapter; diagnostics never affect correctness."""
278
+ try:
279
+ default_diagnostics().record(
280
+ operation,
281
+ client=client,
282
+ duration_ms=duration_ms,
283
+ error=error,
284
+ source_clients=source_clients,
285
+ )
286
+ except Exception:
287
+ return
288
+
289
+
290
+ def record_recall(db: Any, response: Any, *, client: str | None) -> None:
291
+ """Record recall and aggregate cross-client provenance transitions."""
292
+ source_clients: list[str] = []
293
+ fact_ids = [
294
+ str(result.fact.fact_id)
295
+ for result in getattr(response, "results", ())
296
+ if getattr(getattr(result, "fact", None), "fact_id", None)
297
+ ]
298
+ profile_id = ""
299
+ if getattr(response, "results", None):
300
+ profile_id = str(getattr(response.results[0].fact, "profile_id", ""))
301
+ if fact_ids and profile_id:
302
+ try:
303
+ marks = ",".join("?" for _ in fact_ids)
304
+ rows = db.execute(
305
+ f"SELECT DISTINCT created_by FROM provenance "
306
+ f"WHERE profile_id=? AND fact_id IN ({marks})",
307
+ (profile_id, *fact_ids),
308
+ )
309
+ source_clients = [str(dict(row).get("created_by", "")) for row in rows]
310
+ except Exception:
311
+ source_clients = []
312
+ record_operation(
313
+ "recall",
314
+ client=client,
315
+ duration_ms=getattr(response, "retrieval_time_ms", None),
316
+ source_clients=source_clients,
317
+ )
318
+
319
+
320
+ __all__ = [
321
+ "LocalDiagnostics",
322
+ "classify_error",
323
+ "default_diagnostics",
324
+ "normalize_client",
325
+ "record_operation",
326
+ "record_recall",
327
+ ]
@@ -79,6 +79,29 @@ class SlmProcessInfo:
79
79
  age_hours: float
80
80
 
81
81
 
82
+ def is_mcp_server_process(process: SlmProcessInfo) -> bool:
83
+ """Return whether *process* is an MCP stdio server, not an SLM daemon.
84
+
85
+ The startup reaper is deliberately aggressive (zero age threshold), but
86
+ only an orphaned MCP process is safe to reap from ``slm mcp``. A unified
87
+ daemon is intentionally detached from the shell that launched it, so on
88
+ some hosts it has PPID 1 and looks like an orphan. Reaping that daemon at
89
+ MCP startup drops in-flight CLI and dashboard requests.
90
+
91
+ Keep this predicate narrow and based on the command line recorded by the
92
+ OS: every supported MCP entry point includes the standalone ``mcp``
93
+ argument or imports the MCP server module. The daemon's command does
94
+ neither.
95
+ """
96
+ command = process.command.lower()
97
+ return (
98
+ "/slm mcp" in command
99
+ or " slm mcp" in command
100
+ or "superlocalmemory.cli.main mcp" in command
101
+ or "superlocalmemory.mcp.server" in command
102
+ )
103
+
104
+
82
105
  # ---------------------------------------------------------------------------
83
106
  # Windows no-op stubs (AUDIT FIX H0-HIGH-02)
84
107
  # ---------------------------------------------------------------------------
@@ -8,7 +8,7 @@ All adapters run as separate subprocesses managed via PID files.
8
8
  Config stored in ~/.superlocalmemory/adapters.json.
9
9
 
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- License: Elastic-2.0
11
+ License: AGPL-3.0-or-later
12
12
  """
13
13
 
14
14
  from __future__ import annotations
@@ -20,10 +20,12 @@ import subprocess
20
20
  import sys
21
21
  from pathlib import Path
22
22
 
23
+ from superlocalmemory.infra.data_root import canonical_data_root, state_path
24
+
23
25
  logger = logging.getLogger("superlocalmemory.ingestion.manager")
24
26
 
25
- _SLM_HOME = Path.home() / ".superlocalmemory"
26
- _ADAPTERS_CONFIG = _SLM_HOME / "adapters.json"
27
+ _SLM_HOME = None # test-only compatibility override
28
+ _ADAPTERS_CONFIG = None # test-only compatibility override
27
29
  _VALID_ADAPTERS = ("gmail", "calendar", "transcript")
28
30
 
29
31
  # Module paths for each adapter
@@ -34,19 +36,35 @@ _ADAPTER_MODULES = {
34
36
  }
35
37
 
36
38
 
39
+ def _slm_home() -> Path:
40
+ return Path(_SLM_HOME) if _SLM_HOME is not None else canonical_data_root()
41
+
42
+
43
+ def _adapters_config_path() -> Path:
44
+ if _ADAPTERS_CONFIG is not None:
45
+ return Path(_ADAPTERS_CONFIG)
46
+ return state_path("adapters.json")
47
+
48
+
49
+ def _adapter_log_dir() -> Path:
50
+ return _slm_home() / "logs"
51
+
52
+
37
53
  def _load_config() -> dict:
38
- if _ADAPTERS_CONFIG.exists():
39
- return json.loads(_ADAPTERS_CONFIG.read_text())
54
+ config_path = _adapters_config_path()
55
+ if config_path.exists():
56
+ return json.loads(config_path.read_text())
40
57
  return {name: {"enabled": False} for name in _VALID_ADAPTERS}
41
58
 
42
59
 
43
60
  def _save_config(config: dict) -> None:
44
- _ADAPTERS_CONFIG.parent.mkdir(parents=True, exist_ok=True)
45
- _ADAPTERS_CONFIG.write_text(json.dumps(config, indent=2))
61
+ config_path = _adapters_config_path()
62
+ config_path.parent.mkdir(parents=True, exist_ok=True)
63
+ config_path.write_text(json.dumps(config, indent=2))
46
64
 
47
65
 
48
66
  def _pid_file(name: str) -> Path:
49
- return _SLM_HOME / f"adapter-{name}.pid"
67
+ return _slm_home() / f"adapter-{name}.pid"
50
68
 
51
69
 
52
70
  def _is_running(name: str) -> tuple[bool, int | None]:
@@ -128,7 +146,7 @@ def start_adapter(name: str) -> dict:
128
146
  return {"ok": False, "error": f"No module for {name}"}
129
147
 
130
148
  cmd = [sys.executable, "-m", module]
131
- log_dir = _SLM_HOME / "logs"
149
+ log_dir = _adapter_log_dir()
132
150
  log_dir.mkdir(parents=True, exist_ok=True)
133
151
  log_path = log_dir / f"adapter-{name}.log"
134
152
 
@@ -12,12 +12,11 @@ All adapters inherit this. Enforces stateless, safe, cross-platform operation:
12
12
  - Structured logging
13
13
 
14
14
  Part of Qualixar | Author: Varun Pratap Bhardwaj
15
- License: Elastic-2.0
15
+ License: AGPL-3.0-or-later
16
16
  """
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- import json
21
20
  import logging
22
21
  import os
23
22
  import signal
@@ -132,35 +131,30 @@ class BaseAdapter:
132
131
 
133
132
  def _ingest(self, item: IngestItem) -> bool:
134
133
  """POST to daemon /ingest endpoint. Returns True on success."""
135
- try:
136
- import urllib.request
137
- payload = json.dumps({
138
- "content": item.content,
139
- "source_type": self.source_type,
140
- "dedup_key": item.dedup_key,
141
- "metadata": item.metadata if item.metadata else {},
142
- }).encode()
143
- req = urllib.request.Request(
144
- f"{self.daemon_url}/ingest",
145
- data=payload,
146
- headers={"Content-Type": "application/json"},
147
- method="POST",
148
- )
149
- resp = urllib.request.urlopen(req, timeout=30)
150
- data = json.loads(resp.read().decode())
151
- if data.get("ingested"):
152
- self._items_this_hour += 1
153
- self._total_ingested += 1
154
- return True
155
- return False # Already ingested (dedup)
156
- except Exception as exc:
157
- error_str = str(exc)
158
- if "429" in error_str:
159
- logger.info("Daemon returned 429, backing off 5s")
160
- self._stop_event.wait(5)
161
- return self._ingest(item) # Retry once
162
- logger.debug("Ingest failed: %s", exc)
163
- return False
134
+ payload = {
135
+ "content": item.content,
136
+ "source_type": self.source_type,
137
+ "dedup_key": item.dedup_key,
138
+ "metadata": item.metadata if item.metadata else {},
139
+ }
140
+ for attempt in range(2):
141
+ try:
142
+ from superlocalmemory.cli.daemon import daemon_request
143
+
144
+ data = daemon_request("POST", "/ingest", payload)
145
+ if data and data.get("ingested"):
146
+ self._items_this_hour += 1
147
+ self._total_ingested += 1
148
+ return True
149
+ return False # Already ingested (dedup)
150
+ except Exception as exc:
151
+ if "429" in str(exc) and attempt == 0:
152
+ logger.info("Daemon returned 429, backing off 5s")
153
+ self._stop_event.wait(5)
154
+ continue
155
+ logger.debug("Ingest failed: %s", exc)
156
+ return False
157
+ return False
164
158
 
165
159
  def _rate_limited(self) -> bool:
166
160
  if time.time() - self._hour_start > 3600:
@@ -10,7 +10,7 @@ Tier 2: Google Calendar API with OAuth polling — shares Gmail OAuth credential
10
10
  OPT-IN only. Enabled via: slm adapters enable calendar
11
11
 
12
12
  Part of Qualixar | Author: Varun Pratap Bhardwaj
13
- License: Elastic-2.0
13
+ License: AGPL-3.0-or-later
14
14
  """
15
15
 
16
16
  from __future__ import annotations
@@ -22,10 +22,19 @@ from datetime import datetime, timezone
22
22
  from pathlib import Path
23
23
 
24
24
  from superlocalmemory.ingestion.base_adapter import BaseAdapter, AdapterConfig, IngestItem
25
+ from superlocalmemory.infra.data_root import state_path
25
26
 
26
27
  logger = logging.getLogger("superlocalmemory.ingestion.calendar")
27
28
 
28
29
 
30
+ def _adapters_config_path() -> Path:
31
+ return state_path("adapters.json")
32
+
33
+
34
+ def _import_dir() -> Path:
35
+ return state_path("import")
36
+
37
+
29
38
  class CalendarAdapter(BaseAdapter):
30
39
  """Google Calendar ingestion with automatic tier detection."""
31
40
 
@@ -62,7 +71,7 @@ class CalendarAdapter(BaseAdapter):
62
71
  if self._tier != "auto":
63
72
  return
64
73
 
65
- adapters_path = Path.home() / ".superlocalmemory" / "adapters.json"
74
+ adapters_path = _adapters_config_path()
66
75
  cfg = {}
67
76
  if adapters_path.exists():
68
77
  cfg = json.loads(adapters_path.read_text()).get("calendar", {})
@@ -79,7 +88,7 @@ class CalendarAdapter(BaseAdapter):
79
88
  return
80
89
 
81
90
  # Look for ICS files
82
- import_dir = Path.home() / ".superlocalmemory" / "import"
91
+ import_dir = _import_dir()
83
92
  ics_files = list(import_dir.glob("*.ics")) if import_dir.exists() else []
84
93
  if ics_files:
85
94
  self._tier = "ics"
@@ -330,7 +339,7 @@ if __name__ == "__main__":
330
339
  import logging as _logging
331
340
  _logging.basicConfig(level=_logging.INFO, format="%(asctime)s %(message)s")
332
341
 
333
- adapters_path = Path.home() / ".superlocalmemory" / "adapters.json"
342
+ adapters_path = _adapters_config_path()
334
343
  tier = "auto"
335
344
  if adapters_path.exists():
336
345
  cfg = json.loads(adapters_path.read_text()).get("calendar", {})
@@ -8,7 +8,7 @@ Uses OS keychain via keyring library (macOS Keychain, Windows Credential Locker,
8
8
  Linux SecretService). Falls back to file-based storage with restricted permissions.
9
9
 
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- License: Elastic-2.0
11
+ License: AGPL-3.0-or-later
12
12
  """
13
13
 
14
14
  from __future__ import annotations
@@ -19,12 +19,18 @@ import os
19
19
  import sys
20
20
  from pathlib import Path
21
21
 
22
+ from superlocalmemory.infra.data_root import state_path
23
+
22
24
  logger = logging.getLogger("superlocalmemory.ingestion.credentials")
23
25
 
24
- _CRED_DIR = Path.home() / ".superlocalmemory" / "credentials"
26
+ _CRED_DIR = None # test-only compatibility override
25
27
  _SERVICE_PREFIX = "slm"
26
28
 
27
29
 
30
+ def _credential_dir() -> Path:
31
+ return Path(_CRED_DIR) if _CRED_DIR is not None else state_path("credentials")
32
+
33
+
28
34
  def store_credential(service: str, key: str, value: str) -> bool:
29
35
  """Store a credential securely. Returns True on success."""
30
36
  # Try OS keychain first
@@ -38,8 +44,9 @@ def store_credential(service: str, key: str, value: str) -> bool:
38
44
 
39
45
  # Fallback: encrypted file with restricted permissions
40
46
  try:
41
- _CRED_DIR.mkdir(parents=True, exist_ok=True)
42
- cred_file = _CRED_DIR / f"{service}.json"
47
+ cred_dir = _credential_dir()
48
+ cred_dir.mkdir(parents=True, exist_ok=True)
49
+ cred_file = cred_dir / f"{service}.json"
43
50
 
44
51
  existing = {}
45
52
  if cred_file.exists():
@@ -54,7 +61,7 @@ def store_credential(service: str, key: str, value: str) -> bool:
54
61
  # Restrict permissions (Unix only — Windows skipped)
55
62
  if sys.platform != "win32":
56
63
  os.chmod(cred_file, 0o600)
57
- os.chmod(_CRED_DIR, 0o700)
64
+ os.chmod(cred_dir, 0o700)
58
65
 
59
66
  logger.debug("Stored %s/%s in file (keychain unavailable)", service, key)
60
67
  return True
@@ -76,7 +83,7 @@ def load_credential(service: str, key: str) -> str | None:
76
83
 
77
84
  # Fallback: file
78
85
  try:
79
- cred_file = _CRED_DIR / f"{service}.json"
86
+ cred_file = _credential_dir() / f"{service}.json"
80
87
  if cred_file.exists():
81
88
  data = json.loads(cred_file.read_text())
82
89
  return data.get(key)
@@ -100,7 +107,7 @@ def delete_credential(service: str, key: str) -> bool:
100
107
 
101
108
  # Also remove from file
102
109
  try:
103
- cred_file = _CRED_DIR / f"{service}.json"
110
+ cred_file = _credential_dir() / f"{service}.json"
104
111
  if cred_file.exists():
105
112
  data = json.loads(cred_file.read_text())
106
113
  if key in data:
@@ -12,7 +12,7 @@ Tier 3: Gmail API with Pub/Sub push — full GCP (future)
12
12
  OPT-IN only. Enabled via: slm adapters enable gmail
13
13
 
14
14
  Part of Qualixar | Author: Varun Pratap Bhardwaj
15
- License: Elastic-2.0
15
+ License: AGPL-3.0-or-later
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -23,10 +23,19 @@ import sys
23
23
  from pathlib import Path
24
24
 
25
25
  from superlocalmemory.ingestion.base_adapter import BaseAdapter, AdapterConfig, IngestItem
26
+ from superlocalmemory.infra.data_root import state_path
26
27
 
27
28
  logger = logging.getLogger("superlocalmemory.ingestion.gmail")
28
29
 
29
30
 
31
+ def _adapters_config_path() -> Path:
32
+ return state_path("adapters.json")
33
+
34
+
35
+ def _import_dir() -> Path:
36
+ return state_path("import")
37
+
38
+
30
39
  class GmailAdapter(BaseAdapter):
31
40
  """Gmail ingestion with automatic tier detection."""
32
41
 
@@ -71,7 +80,7 @@ class GmailAdapter(BaseAdapter):
71
80
  if self._tier != "auto":
72
81
  return
73
82
 
74
- adapters_path = Path.home() / ".superlocalmemory" / "adapters.json"
83
+ adapters_path = _adapters_config_path()
75
84
  cfg = {}
76
85
  if adapters_path.exists():
77
86
  cfg = json.loads(adapters_path.read_text()).get("gmail", {})
@@ -93,7 +102,7 @@ class GmailAdapter(BaseAdapter):
93
102
  return
94
103
 
95
104
  # Default: look for mbox file
96
- mbox_dir = Path.home() / ".superlocalmemory" / "import"
105
+ mbox_dir = _import_dir()
97
106
  mbox_files = list(mbox_dir.glob("*.mbox")) if mbox_dir.exists() else []
98
107
  if mbox_files:
99
108
  self._tier = "mbox"
@@ -359,7 +368,7 @@ if __name__ == "__main__":
359
368
  import logging as _logging
360
369
  _logging.basicConfig(level=_logging.INFO, format="%(asctime)s %(message)s")
361
370
 
362
- adapters_path = Path.home() / ".superlocalmemory" / "adapters.json"
371
+ adapters_path = _adapters_config_path()
363
372
  tier = "auto"
364
373
  if adapters_path.exists():
365
374
  cfg = json.loads(adapters_path.read_text()).get("gmail", {})