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.
- package/CHANGELOG.md +52 -0
- package/README.md +275 -72
- package/bin/slm-npm +43 -89
- package/docs/pi-dev-integration.md +43 -0
- package/ide/configs/antigravity-mcp.json +2 -2
- package/ide/configs/chatgpt-desktop-mcp.json +1 -1
- package/ide/configs/claude-desktop-mcp.json +2 -2
- package/ide/configs/windsurf-mcp.json +2 -2
- package/ide/hooks/context-hook.js +6 -2
- package/ide/hooks/post-recall-hook.js +7 -3
- package/ide/hooks/tool-event-hook.sh +2 -1
- package/package.json +19 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/_GENERATED.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/pyproject.toml +40 -8
- package/scripts/postinstall-interactive.js +17 -94
- package/scripts/postinstall.js +185 -258
- package/scripts/preuninstall.js +9 -50
- package/src/superlocalmemory/__init__.py +2 -2
- package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
- package/src/superlocalmemory/attribution/signer.py +34 -19
- package/src/superlocalmemory/attribution/watermark.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +3 -5
- package/src/superlocalmemory/cli/commands.py +490 -195
- package/src/superlocalmemory/cli/context_commands.py +5 -4
- package/src/superlocalmemory/cli/daemon.py +282 -187
- package/src/superlocalmemory/cli/db_migrate.py +3 -1
- package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
- package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
- package/src/superlocalmemory/cli/main.py +128 -31
- package/src/superlocalmemory/cli/pending_store.py +54 -38
- package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
- package/src/superlocalmemory/cli/service_installer.py +57 -52
- package/src/superlocalmemory/cli/setup_wizard.py +142 -88
- package/src/superlocalmemory/cli/version_banner.py +2 -1
- package/src/superlocalmemory/code_graph/config.py +3 -1
- package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
- package/src/superlocalmemory/core/config.py +65 -20
- package/src/superlocalmemory/core/consolidation_engine.py +9 -7
- package/src/superlocalmemory/core/context_cache.py +56 -8
- package/src/superlocalmemory/core/derivation_lineage.py +246 -0
- package/src/superlocalmemory/core/embedding_worker.py +32 -20
- package/src/superlocalmemory/core/embeddings.py +54 -18
- package/src/superlocalmemory/core/engine.py +150 -104
- package/src/superlocalmemory/core/engine_ingestion.py +513 -0
- package/src/superlocalmemory/core/engine_wiring.py +2 -0
- package/src/superlocalmemory/core/evidence_bundle.py +526 -0
- package/src/superlocalmemory/core/fact_consolidator.py +5 -11
- package/src/superlocalmemory/core/graph_analyzer.py +2 -2
- package/src/superlocalmemory/core/health_monitor.py +4 -2
- package/src/superlocalmemory/core/ingestion_command.py +636 -0
- package/src/superlocalmemory/core/injection.py +69 -18
- package/src/superlocalmemory/core/lifecycle_state.py +153 -0
- package/src/superlocalmemory/core/maintenance.py +23 -22
- package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
- package/src/superlocalmemory/core/mutations.py +143 -0
- package/src/superlocalmemory/core/platform_utils.py +7 -4
- package/src/superlocalmemory/core/ram_lock.py +16 -5
- package/src/superlocalmemory/core/rate_limit.py +1 -1
- package/src/superlocalmemory/core/recall_pipeline.py +60 -101
- package/src/superlocalmemory/core/recall_worker.py +76 -59
- package/src/superlocalmemory/core/registry.py +1 -1
- package/src/superlocalmemory/core/scale_engine.py +293 -0
- package/src/superlocalmemory/core/score_contract.py +62 -0
- package/src/superlocalmemory/core/security_primitives.py +3 -1
- package/src/superlocalmemory/core/slm_disabled.py +3 -5
- package/src/superlocalmemory/core/store_pipeline.py +172 -40
- package/src/superlocalmemory/core/tier_manager.py +32 -20
- package/src/superlocalmemory/core/worker_pool.py +13 -4
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
- package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
- package/src/superlocalmemory/encoding/auto_linker.py +1 -1
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
- package/src/superlocalmemory/encoding/consolidator.py +22 -5
- package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
- package/src/superlocalmemory/encoding/foresight.py +2 -0
- package/src/superlocalmemory/encoding/graph_builder.py +1 -1
- package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
- package/src/superlocalmemory/evaluation/__init__.py +13 -0
- package/src/superlocalmemory/evaluation/calibration.py +308 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
- package/src/superlocalmemory/graph/cozo_backend.py +256 -23
- package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
- package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
- package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
- package/src/superlocalmemory/hooks/auto_recall.py +31 -6
- package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
- package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
- package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
- package/src/superlocalmemory/hooks/codex_assets.py +59 -0
- package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
- package/src/superlocalmemory/hooks/context_payload.py +1 -1
- package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
- package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
- package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
- package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
- package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
- package/src/superlocalmemory/hooks/session_registry.py +15 -8
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
- package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
- package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
- package/src/superlocalmemory/infra/auth_middleware.py +38 -5
- package/src/superlocalmemory/infra/backup.py +7 -5
- package/src/superlocalmemory/infra/cloud_backup.py +18 -8
- package/src/superlocalmemory/infra/daemon_identity.py +248 -0
- package/src/superlocalmemory/infra/data_root.py +199 -0
- package/src/superlocalmemory/infra/event_bus.py +3 -1
- package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
- package/src/superlocalmemory/infra/process_reaper.py +23 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
- package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
- package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/credentials.py +14 -7
- package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
- package/src/superlocalmemory/learning/ensemble.py +11 -0
- package/src/superlocalmemory/learning/entity_compiler.py +1 -1
- package/src/superlocalmemory/learning/feedback.py +1 -1
- package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
- package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
- package/src/superlocalmemory/learning/ranker.py +4 -1
- package/src/superlocalmemory/learning/source_quality.py +1 -1
- package/src/superlocalmemory/learning/trigram_index.py +3 -2
- package/src/superlocalmemory/llm/backbone.py +13 -8
- package/src/superlocalmemory/math/ebbinghaus.py +1 -1
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/fisher_quantized.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +1 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/polar_quant.py +3 -4
- package/src/superlocalmemory/math/qjl.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +1 -1
- package/src/superlocalmemory/math/turbo_quant.py +3 -2
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
- package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
- package/src/superlocalmemory/mcp/http_transport.py +53 -0
- package/src/superlocalmemory/mcp/server.py +39 -13
- package/src/superlocalmemory/mcp/shared.py +69 -3
- package/src/superlocalmemory/mcp/tools_active.py +141 -31
- package/src/superlocalmemory/mcp/tools_core.py +128 -29
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
- package/src/superlocalmemory/mcp/tools_learning.py +42 -2
- package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
- package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
- package/src/superlocalmemory/mcp/tools_v28.py +23 -2
- package/src/superlocalmemory/mcp/tools_v3.py +26 -1
- package/src/superlocalmemory/mcp/tools_v33.py +56 -17
- package/src/superlocalmemory/mesh/broker.py +2 -0
- package/src/superlocalmemory/mesh/remote_sync.py +50 -12
- package/src/superlocalmemory/optimize/cache/manager.py +77 -1
- package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
- package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
- package/src/superlocalmemory/optimize/compress/router.py +6 -1
- package/src/superlocalmemory/optimize/config/__init__.py +5 -0
- package/src/superlocalmemory/optimize/config/store.py +6 -4
- package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
- package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
- package/src/superlocalmemory/optimize/proxy/server.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +14 -13
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
- package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
- package/src/superlocalmemory/retrieval/engine.py +169 -79
- package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
- package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
- package/src/superlocalmemory/retrieval/fusion.py +1 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
- package/src/superlocalmemory/retrieval/reranker.py +56 -20
- package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
- package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
- package/src/superlocalmemory/retrieval/strategy.py +1 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/api.py +10 -7
- package/src/superlocalmemory/server/bandit_loops.py +4 -2
- package/src/superlocalmemory/server/recall_serializer.py +24 -0
- package/src/superlocalmemory/server/route_mutations.py +84 -0
- package/src/superlocalmemory/server/routes/agents.py +8 -6
- package/src/superlocalmemory/server/routes/brain.py +14 -12
- package/src/superlocalmemory/server/routes/chat.py +29 -12
- package/src/superlocalmemory/server/routes/data_io.py +55 -24
- package/src/superlocalmemory/server/routes/helpers.py +29 -4
- package/src/superlocalmemory/server/routes/ingest.py +53 -36
- package/src/superlocalmemory/server/routes/memories.py +104 -43
- package/src/superlocalmemory/server/routes/mesh.py +31 -0
- package/src/superlocalmemory/server/routes/profiles.py +26 -4
- package/src/superlocalmemory/server/routes/tiers.py +43 -11
- package/src/superlocalmemory/server/routes/timeline.py +5 -1
- package/src/superlocalmemory/server/routes/v3_api.py +76 -21
- package/src/superlocalmemory/server/security_middleware.py +1 -1
- package/src/superlocalmemory/server/ui.py +6 -3
- package/src/superlocalmemory/server/unified_daemon.py +680 -293
- package/src/superlocalmemory/server/write_identity.py +147 -0
- package/src/superlocalmemory/storage/access_log.py +4 -3
- package/src/superlocalmemory/storage/database.py +118 -25
- package/src/superlocalmemory/storage/migration_runner.py +84 -1
- package/src/superlocalmemory/storage/migration_v33.py +1 -1
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
- package/src/superlocalmemory/storage/models.py +16 -0
- package/src/superlocalmemory/storage/quantized_store.py +20 -3
- package/src/superlocalmemory/storage/v2_migrator.py +5 -3
- package/src/superlocalmemory/ui/favicon.svg +5 -0
- package/src/superlocalmemory/ui/index.html +1 -0
- package/src/superlocalmemory/ui/js/compliance.js +1 -1
- package/src/superlocalmemory/ui/js/core.js +49 -8
- package/src/superlocalmemory/ui/js/dashboard.js +23 -2
- package/src/superlocalmemory/ui/js/feedback.js +1 -1
- package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
- package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
- package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
- package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
- package/src/superlocalmemory/ui/js/settings.js +4 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
- package/bin/slm +0 -59
- package/bin/slm.bat +0 -77
- package/bin/slm.cmd +0 -5
- package/ide/integrations/langchain/README.md +0 -106
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
- package/ide/integrations/langchain/pyproject.toml +0 -38
- package/ide/integrations/langchain/tests/__init__.py +0 -3
- package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
- package/ide/integrations/langchain/tests/test_security.py +0 -117
- package/ide/integrations/llamaindex/README.md +0 -81
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
- package/ide/integrations/llamaindex/pyproject.toml +0 -43
- package/ide/integrations/llamaindex/tests/__init__.py +0 -3
- package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
- package/ide/integrations/llamaindex/tests/test_security.py +0 -241
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -15
- package/plugin-src/scripts/slm-launch.bat +0 -17
- package/plugin-src/settings.json +0 -16
- package/plugin-src/skills/slm-cache/SKILL.md +0 -140
- package/plugin-src/skills/slm-compress/SKILL.md +0 -143
- package/plugin-src/skills/slm-graph/SKILL.md +0 -300
- package/plugin-src/skills/slm-recall/SKILL.md +0 -204
- package/plugin-src/skills/slm-remember/SKILL.md +0 -194
- package/plugin-src/skills/slm-session/SKILL.md +0 -207
- package/plugin-src/skills/slm-status/SKILL.md +0 -149
- package/scripts/__tests__/build-plugin.test.mjs +0 -613
- package/scripts/_savings_math.py +0 -270
- package/scripts/build-dmg.sh +0 -417
- package/scripts/build-plugin.js +0 -742
- package/scripts/build-slm-hook.ps1 +0 -40
- package/scripts/build-slm-hook.sh +0 -45
- package/scripts/build_entry.py +0 -452
- package/scripts/ci/stage5b_gate.sh +0 -50
- package/scripts/dogfood_savings.py +0 -490
- package/scripts/generate-thumbnails.py +0 -218
- package/scripts/install-skills.ps1 +0 -4
- package/scripts/install-skills.sh +0 -5
- package/scripts/install.ps1 +0 -701
- package/scripts/install.sh +0 -1015
- package/scripts/postinstall_binary.js +0 -287
- package/scripts/prepack.js +0 -33
- package/scripts/release_manifest.py +0 -273
- package/scripts/slm-hook.spec +0 -56
- package/scripts/start-dashboard.ps1 +0 -52
- package/scripts/start-dashboard.sh +0 -41
- package/scripts/sync-wiki.ps1 +0 -127
- package/scripts/sync-wiki.sh +0 -82
- package/scripts/test-dmg.sh +0 -161
- package/scripts/test-npm-package.ps1 +0 -252
- package/scripts/test-npm-package.sh +0 -207
- package/scripts/verify-install.ps1 +0 -294
- package/scripts/verify-install.sh +0 -266
- package/scripts/verify-v27.ps1 +0 -301
- package/scripts/verify-v27.sh +0 -233
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -71
- 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:
|
|
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 =
|
|
26
|
-
_ADAPTERS_CONFIG =
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
data=payload
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
42
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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", {})
|