smartmemory-mcp 0.2.4__tar.gz

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 (60) hide show
  1. smartmemory_mcp-0.2.4/PKG-INFO +10 -0
  2. smartmemory_mcp-0.2.4/README.md +34 -0
  3. smartmemory_mcp-0.2.4/pyproject.toml +20 -0
  4. smartmemory_mcp-0.2.4/setup.cfg +4 -0
  5. smartmemory_mcp-0.2.4/smartmemory_mcp/__init__.py +0 -0
  6. smartmemory_mcp-0.2.4/smartmemory_mcp/backends/__init__.py +0 -0
  7. smartmemory_mcp-0.2.4/smartmemory_mcp/backends/dispatch.py +86 -0
  8. smartmemory_mcp-0.2.4/smartmemory_mcp/backends/interface.py +170 -0
  9. smartmemory_mcp-0.2.4/smartmemory_mcp/backends/local.py +327 -0
  10. smartmemory_mcp-0.2.4/smartmemory_mcp/backends/models.py +75 -0
  11. smartmemory_mcp-0.2.4/smartmemory_mcp/backends/remote.py +460 -0
  12. smartmemory_mcp-0.2.4/smartmemory_mcp/code_parser.py +277 -0
  13. smartmemory_mcp-0.2.4/smartmemory_mcp/eval_logger.py +88 -0
  14. smartmemory_mcp-0.2.4/smartmemory_mcp/server.py +176 -0
  15. smartmemory_mcp-0.2.4/smartmemory_mcp/tier.py +109 -0
  16. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/__init__.py +0 -0
  17. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/agent_tools.py +132 -0
  18. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/anchor_tools.py +107 -0
  19. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/code_tools.py +432 -0
  20. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/common.py +63 -0
  21. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/decision_tools.py +405 -0
  22. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/dev_tools.py +337 -0
  23. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/evolution_tools.py +102 -0
  24. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/graph_tools.py +105 -0
  25. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/insight_tools.py +200 -0
  26. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/lifecycle_tools.py +112 -0
  27. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/memory_tools.py +746 -0
  28. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/pattern_tools.py +123 -0
  29. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/plan_tools.py +124 -0
  30. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/portability_tools.py +188 -0
  31. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/reasoning_tools.py +247 -0
  32. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/structured_tools.py +54 -0
  33. smartmemory_mcp-0.2.4/smartmemory_mcp/tools/zettel_tools.py +121 -0
  34. smartmemory_mcp-0.2.4/smartmemory_mcp.egg-info/PKG-INFO +10 -0
  35. smartmemory_mcp-0.2.4/smartmemory_mcp.egg-info/SOURCES.txt +58 -0
  36. smartmemory_mcp-0.2.4/smartmemory_mcp.egg-info/dependency_links.txt +1 -0
  37. smartmemory_mcp-0.2.4/smartmemory_mcp.egg-info/entry_points.txt +2 -0
  38. smartmemory_mcp-0.2.4/smartmemory_mcp.egg-info/requires.txt +5 -0
  39. smartmemory_mcp-0.2.4/smartmemory_mcp.egg-info/top_level.txt +1 -0
  40. smartmemory_mcp-0.2.4/tests/test_agent_evaluation_get_contract.py +146 -0
  41. smartmemory_mcp-0.2.4/tests/test_backend_dispatch.py +135 -0
  42. smartmemory_mcp-0.2.4/tests/test_citations.py +80 -0
  43. smartmemory_mcp-0.2.4/tests/test_code_blame_tool.py +165 -0
  44. smartmemory_mcp-0.2.4/tests/test_code_read_transcript_tool.py +129 -0
  45. smartmemory_mcp-0.2.4/tests/test_confidence_display.py +90 -0
  46. smartmemory_mcp-0.2.4/tests/test_decision_tools_local_backend.py +93 -0
  47. smartmemory_mcp-0.2.4/tests/test_dist_lite_quiet_local_origin.py +89 -0
  48. smartmemory_mcp-0.2.4/tests/test_get_working_context_standalone.py +212 -0
  49. smartmemory_mcp-0.2.4/tests/test_graph_tools_local_backend.py +284 -0
  50. smartmemory_mcp-0.2.4/tests/test_lifecycle_tools.py +18 -0
  51. smartmemory_mcp-0.2.4/tests/test_lineage_display.py +40 -0
  52. smartmemory_mcp-0.2.4/tests/test_normalize.py +149 -0
  53. smartmemory_mcp-0.2.4/tests/test_pending_decision_tools_local_backend.py +159 -0
  54. smartmemory_mcp-0.2.4/tests/test_portability.py +289 -0
  55. smartmemory_mcp-0.2.4/tests/test_read_around_tool.py +123 -0
  56. smartmemory_mcp-0.2.4/tests/test_remote_error_surfacing.py +79 -0
  57. smartmemory_mcp-0.2.4/tests/test_server_decompose.py +48 -0
  58. smartmemory_mcp-0.2.4/tests/test_server_multi_hop.py +106 -0
  59. smartmemory_mcp-0.2.4/tests/test_stale_display.py +81 -0
  60. smartmemory_mcp-0.2.4/tests/test_tier_registration.py +135 -0
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: smartmemory-mcp
3
+ Version: 0.2.4
4
+ Summary: Unified SmartMemory MCP server — tiered tools, local + remote backends
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: fastmcp>=2.0
8
+ Requires-Dist: httpx>=0.27
9
+ Provides-Extra: keyring
10
+ Requires-Dist: keyring>=24.0; extra == "keyring"
@@ -0,0 +1,34 @@
1
+ # smart-memory-mcp
2
+
3
+ Unified SmartMemory MCP (Model Context Protocol) server — tiered tools, local + remote backends.
4
+
5
+ ## Overview
6
+
7
+ MCP server exposing SmartMemory operations to MCP-compatible clients (Claude Desktop, Cursor, etc.). Implements the full memory toolset (add, search, recall, decisions, plans, anchors, code-index) and routes to either a local SmartMemory instance or a remote `smart-memory-service` API endpoint.
8
+
9
+ ## Status
10
+
11
+ **Version:** 0.2.1
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ pip install -e .
17
+
18
+ # Run the server
19
+ smartmemory-mcp # or python -m smartmemory_mcp
20
+ ```
21
+
22
+ Tests:
23
+
24
+ ```bash
25
+ pytest tests/ -v
26
+ ```
27
+
28
+ ## Documentation
29
+
30
+ Full SmartMemory documentation: https://docs.smartmemory.ai
31
+
32
+ ## Part of SmartMemory
33
+
34
+ This is one component of the SmartMemory ecosystem. See the [main repo](https://github.com/smart-memory/smart-memory) for the broader project.
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "smartmemory-mcp"
7
+ version = "0.2.4"
8
+ description = "Unified SmartMemory MCP server — tiered tools, local + remote backends"
9
+ requires-python = ">=3.10"
10
+ license = {text = "MIT"}
11
+ dependencies = [
12
+ "fastmcp>=2.0",
13
+ "httpx>=0.27",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ keyring = ["keyring>=24.0"]
18
+
19
+ [project.scripts]
20
+ smartmemory-mcp = "smartmemory_mcp.server:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,86 @@
1
+ """Backend dispatch — resolve local or remote backend based on config.
2
+
3
+ Local-first: if smartmemory package is installed and mode != "remote", use LocalBackend.
4
+ Remote: if mode == "remote" or only env vars available, use RemoteBackend.
5
+ Result cached in module-level _backend after first resolution.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from smartmemory_mcp.backends.interface import MemoryBackend
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+ _backend: "MemoryBackend | None" = None
19
+
20
+
21
+ def resolve_backend() -> "MemoryBackend":
22
+ """Return the active backend, creating it on first call.
23
+
24
+ Resolution order:
25
+ 1. Try importing smartmemory_app.config.load_config — if available, read mode.
26
+ - mode == "remote" -> RemoteBackend with config values
27
+ - mode == "local" or mode is set -> LocalBackend
28
+ 2. If smartmemory not installed, check SMARTMEMORY_API_KEY env var -> RemoteBackend.
29
+ 3. No backend resolvable -> raise RuntimeError.
30
+ """
31
+ global _backend
32
+ if _backend is not None:
33
+ return _backend
34
+
35
+ # Path 1: smartmemory package installed — use its config system
36
+ try:
37
+ from smartmemory_app.config import load_config
38
+
39
+ cfg = load_config()
40
+ if cfg.mode == "remote":
41
+ log.info("Backend dispatch: remote mode (from config)")
42
+ from smartmemory_mcp.backends.remote import RemoteBackend
43
+ from smartmemory_mcp.tier import get_api_key
44
+
45
+ _backend = RemoteBackend(
46
+ api_url=cfg.api_url,
47
+ api_key=get_api_key(), # Reads env var → keyring → file
48
+ team_id=cfg.team_id,
49
+ )
50
+ return _backend
51
+
52
+ # Local mode (mode == "local" or mode is set to anything else, or None with package available)
53
+ log.info("Backend dispatch: local mode (smartmemory installed)")
54
+ from smartmemory_mcp.backends.local import LocalBackend
55
+
56
+ _backend = LocalBackend()
57
+ return _backend
58
+
59
+ except ImportError:
60
+ pass # smartmemory package not installed — fall through to env var path
61
+
62
+ # Path 2: No smartmemory package — check for stored/env API key
63
+ from smartmemory_mcp.tier import get_api_key
64
+
65
+ api_key = get_api_key()
66
+ if api_key:
67
+ log.info("Backend dispatch: remote mode (from stored API key)")
68
+ from smartmemory_mcp.backends.remote import RemoteBackend
69
+
70
+ _backend = RemoteBackend(api_key=api_key)
71
+ return _backend
72
+
73
+ # Path 3: No backend resolvable
74
+ raise RuntimeError(
75
+ "No SmartMemory backend available.\n"
76
+ "Either:\n"
77
+ " 1. Install the full package: pip install smartmemory\n"
78
+ " 2. Set SMARTMEMORY_API_KEY env var for remote mode\n"
79
+ " 3. Run: smartmemory setup"
80
+ )
81
+
82
+
83
+ def reset_backend() -> None:
84
+ """Clear the cached backend. Used by tests to force re-resolution."""
85
+ global _backend
86
+ _backend = None
@@ -0,0 +1,170 @@
1
+ """MemoryBackend protocol — structural interface for local and remote backends."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Protocol, runtime_checkable
5
+
6
+ from .models import MemoryResult
7
+
8
+
9
+ @runtime_checkable
10
+ class MemoryBackend(Protocol):
11
+ """Duck-typed interface that all backend implementations must satisfy.
12
+
13
+ Tools call these methods without knowing whether the backend is local (in-process
14
+ SmartMemory) or remote (REST API via httpx). Methods use **kwargs liberally so
15
+ backends can accept extra parameters without protocol changes.
16
+ """
17
+
18
+ # --- Core CRUD ---------------------------------------------------------------
19
+
20
+ def add(self, content: str, memory_type: str = "semantic", **kwargs: Any) -> dict[str, Any]:
21
+ """Store a memory item."""
22
+ ...
23
+
24
+ def get(self, item_id: str, **kwargs: Any) -> MemoryResult | None:
25
+ """Get a single memory by ID."""
26
+ ...
27
+
28
+ def update(self, item_id: str, **kwargs: Any) -> dict[str, Any]:
29
+ """Update a memory item."""
30
+ ...
31
+
32
+ def delete(self, item_id: str, **kwargs: Any) -> dict[str, Any]:
33
+ """Delete a memory item."""
34
+ ...
35
+
36
+ # --- Search & Recall ---------------------------------------------------------
37
+
38
+ def search(self, query: str, top_k: int = 5, **kwargs: Any) -> list[MemoryResult]:
39
+ """Semantic similarity search."""
40
+ ...
41
+
42
+ def search_by_metadata(self, metadata_key: str, metadata_value: str, top_k: int = 10, **kwargs: Any) -> list[MemoryResult]:
43
+ """Search by metadata key-value match."""
44
+ ...
45
+
46
+ def recall(self, cwd: str | None = None, top_k: int = 10, **kwargs: Any) -> str:
47
+ """Recall recent and relevant memories, formatted as markdown."""
48
+ ...
49
+
50
+ # --- Pipeline ----------------------------------------------------------------
51
+
52
+ def ingest(self, content: str, memory_type: str = "semantic", **kwargs: Any) -> dict[str, Any] | str:
53
+ """Ingest content through the full extraction pipeline."""
54
+ ...
55
+
56
+ def ingest_structured(self, data: dict[str, Any], schema: str | None = None, **kwargs: Any) -> str:
57
+ """Ingest structured data with an optional schema."""
58
+ ...
59
+
60
+ def ingest_conversation_sync(
61
+ self,
62
+ turns: list,
63
+ session_boundaries: list | None = None,
64
+ conversation_id: str | None = None,
65
+ session_dates: list | None = None,
66
+ turns_per_chunk: int = 15,
67
+ max_chunk_chars: int = 12000,
68
+ max_concurrent: int = 4,
69
+ **kwargs: Any,
70
+ ) -> dict[str, Any]:
71
+ """Ingest a conversation as session chunks (RLM-1g)."""
72
+ ...
73
+
74
+ # --- Collection operations ---------------------------------------------------
75
+
76
+ def list_memories(self, **kwargs: Any) -> list[MemoryResult]:
77
+ """List all memory items."""
78
+ ...
79
+
80
+ def clear_user_memories(self, **kwargs: Any) -> dict[str, Any]:
81
+ """Delete all memories for the current user."""
82
+ ...
83
+
84
+ def get_all_items_debug(self, **kwargs: Any) -> list[dict[str, Any]]:
85
+ """Return all items including internal nodes (debug only)."""
86
+ ...
87
+
88
+ def stats(self, **kwargs: Any) -> dict[str, Any]:
89
+ """Return memory statistics (total, by type, health score)."""
90
+ ...
91
+
92
+ # --- Evolution & clustering --------------------------------------------------
93
+
94
+ def run_evolution_cycle(self, **kwargs: Any) -> dict[str, Any]:
95
+ """Run a full evolution cycle across all evolvers."""
96
+ ...
97
+
98
+ # CORE-MEMORY-DYNAMICS-1 M1b: commit_working_to_episodic / commit_working_to_procedural
99
+ # removed from the protocol. The core façades are gone — the ConsolidationRouter
100
+ # now routes pending items at ingest. Backends that previously implemented these
101
+ # methods should drop them; callers should use add()/ingest() with
102
+ # memory_type="pending".
103
+
104
+ def run_evolver(self, evolver_name: str, **kwargs: Any) -> dict[str, Any]:
105
+ """Run a specific evolver by name."""
106
+ ...
107
+
108
+ def run_clustering(self, **kwargs: Any) -> dict[str, Any]:
109
+ """Run clustering analysis on stored memories."""
110
+ ...
111
+
112
+ # --- Insight & reflection ----------------------------------------------------
113
+
114
+ def reflect(self, **kwargs: Any) -> dict[str, Any]:
115
+ """Generate reflections from stored memories."""
116
+ ...
117
+
118
+ def summary(self, **kwargs: Any) -> dict[str, Any]:
119
+ """Generate a summary of stored memories."""
120
+ ...
121
+
122
+ def orphaned_notes(self, **kwargs: Any) -> list[MemoryResult]:
123
+ """Find notes without links to other memories."""
124
+ ...
125
+
126
+ def find_old_notes(self, **kwargs: Any) -> list[MemoryResult]:
127
+ """Find notes that haven't been accessed recently."""
128
+ ...
129
+
130
+ # --- Personalization ---------------------------------------------------------
131
+
132
+ def personalize(self, query: str, **kwargs: Any) -> dict[str, Any]:
133
+ """Personalize a response using stored memory context."""
134
+ ...
135
+
136
+ def update_from_feedback(self, item_id: str, feedback: str, **kwargs: Any) -> dict[str, Any]:
137
+ """Update a memory item based on user feedback."""
138
+ ...
139
+
140
+ # --- Grounding & linking -----------------------------------------------------
141
+
142
+ def ground(self, item_id: str, **kwargs: Any) -> dict[str, Any]:
143
+ """Ground a memory item with external references."""
144
+ ...
145
+
146
+ def link(self, source_id: str, target_id: str, **kwargs: Any) -> dict[str, Any]:
147
+ """Create a link between two memory items."""
148
+ ...
149
+
150
+ def add_edge(self, source_id: str, target_id: str, relation: str, **kwargs: Any) -> dict[str, Any]:
151
+ """Add a typed edge between two items in the knowledge graph."""
152
+ ...
153
+
154
+ def get_links(self, item_id: str, **kwargs: Any) -> list[MemoryResult]:
155
+ """Get all links for a memory item."""
156
+ ...
157
+
158
+ def get_neighbors(self, item_id: str, **kwargs: Any) -> dict[str, Any]:
159
+ """Get neighboring nodes in the knowledge graph."""
160
+ ...
161
+
162
+ def find_shortest_path(self, source_id: str, target_id: str, **kwargs: Any) -> dict[str, Any]:
163
+ """Find the shortest path between two nodes in the graph."""
164
+ ...
165
+
166
+ # --- Retrieval feedback (SELF-IMPROVE-6) -------------------------------------
167
+
168
+ def submit_feedback(self, search_session_id: str, result_used: list[str], **kwargs: Any) -> dict[str, Any]:
169
+ """Submit result-selection feedback for a completed search session."""
170
+ ...
@@ -0,0 +1,327 @@
1
+ """Local backend — delegates to smartmemory_app.storage (optional dependency)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from .models import MemoryResult, normalize_item, normalize_items
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class LocalBackend:
14
+ """Wraps smartmemory package for local-mode operations."""
15
+
16
+ def __init__(self) -> None:
17
+ try:
18
+ from smartmemory_app.storage import get_memory
19
+ self._get_memory = get_memory
20
+ self._mem = get_memory()
21
+ except ImportError:
22
+ raise RuntimeError(
23
+ "Local backend requires the smartmemory package.\n"
24
+ "Install with: pip install smartmemory"
25
+ )
26
+
27
+ # -- Core CRUD --
28
+
29
+ def add(self, content: str, memory_type: str = "semantic", metadata: dict | None = None, **kwargs: Any) -> str:
30
+ """Store a memory item."""
31
+ from smartmemory.models.memory_item import MemoryItem
32
+ # DIST-LITE-QUIET-1: attribute the write so it lands as tier-2 user content, not
33
+ # origin='unknown' (tier 4, hidden from recall+search). An explicit origin in
34
+ # metadata (e.g. an importer) wins; default to this producer.
35
+ meta = dict(metadata or {})
36
+ origin = meta.pop("origin", None) or kwargs.pop("origin", None) or "mcp:memory_add"
37
+ item = MemoryItem(content=content, memory_type=memory_type, metadata=meta, origin=origin)
38
+ return self._mem.add(item)
39
+
40
+ def get(self, item_id: str, **kwargs: Any) -> MemoryResult | None:
41
+ """Retrieve a memory by ID."""
42
+ result = self._mem.get(item_id)
43
+ if result is None:
44
+ return None
45
+ return normalize_item(result)
46
+
47
+ def update(
48
+ self,
49
+ item_id: str,
50
+ content: str | None = None,
51
+ metadata: dict | None = None,
52
+ properties: dict | None = None,
53
+ write_mode: str | None = None,
54
+ **kwargs: Any,
55
+ ) -> str:
56
+ """Update an existing memory via the canonical update_properties path.
57
+
58
+ See CORE-CRUD-UPDATE-1 contract: `properties` takes precedence over the
59
+ content/metadata conveniences; `write_mode` is "merge" (default) or
60
+ "replace".
61
+ """
62
+ item = self._mem.get(item_id)
63
+ if item is None:
64
+ return f"Not found: {item_id}"
65
+
66
+ if properties is not None:
67
+ props = dict(properties)
68
+ else:
69
+ props = {}
70
+ if content is not None:
71
+ props["content"] = content
72
+ if metadata is not None:
73
+ existing_meta = {}
74
+ if hasattr(item, "metadata") and isinstance(item.metadata, dict):
75
+ existing_meta = item.metadata
76
+ elif isinstance(item, dict):
77
+ existing_meta = item.get("metadata") or {}
78
+ props["metadata"] = {**existing_meta, **metadata}
79
+
80
+ if not props:
81
+ return "At least one of 'content', 'metadata', or 'properties' must be provided"
82
+
83
+ self._mem.update_properties(item_id, props, write_mode=write_mode)
84
+ return f"Updated: {item_id}"
85
+
86
+ def delete(self, item_id: str, **kwargs: Any) -> bool:
87
+ """Delete a memory by ID."""
88
+ return self._mem.delete(item_id)
89
+
90
+ # -- Search --
91
+
92
+ def search(self, query: str, top_k: int = 5, **kwargs: Any) -> list[MemoryResult]:
93
+ """Semantic search."""
94
+ from smartmemory_app.storage import search
95
+ results = normalize_items(search(query, top_k, **kwargs))
96
+ # SELF-IMPROVE-6: track shown IDs for local-mode feedback
97
+ self._last_search_session_id = f"local:{id(results)}:{top_k}"
98
+ self._last_shown_ids = [r.get("item_id", "") for r in results if r.get("item_id")]
99
+ return results
100
+
101
+ def search_by_metadata(self, metadata_key: str, metadata_value: str, top_k: int = 10, **kwargs: Any) -> list[MemoryResult]:
102
+ """Search by metadata field."""
103
+ return normalize_items(self._mem.search_by_metadata(metadata_key, metadata_value, top_k=top_k))
104
+
105
+ def blame_code(self, **kwargs: Any) -> dict[str, Any]:
106
+ """Code-provenance blame passthrough (CORE-CODE-PROVENANCE-1 Phase 2b).
107
+
108
+ Delegates to the in-process lite SmartMemory, which reads the same local
109
+ store the capture hook wrote to. Returns the forge-shaped BlameResult dict;
110
+ raises ValueError on a git error (the tool maps it to a graceful string).
111
+ """
112
+ return self._mem.blame_code(**kwargs)
113
+
114
+ def read_transcript_centered(self, **kwargs: Any) -> dict[str, Any]:
115
+ """Centered transcript-reader passthrough (CORE-CODE-PROVENANCE-1 Phase 2c).
116
+
117
+ The read half of the blame->read chain. Delegates to the in-process lite
118
+ SmartMemory, which reads the raw transcript JSONL on the local filesystem.
119
+ Local-only — the hosted REST surface is parked.
120
+ """
121
+ return self._mem.read_transcript_centered(**kwargs)
122
+
123
+ # -- Ingest & Recall --
124
+
125
+ def ingest(self, content: str, memory_type: str = "episodic", **kwargs: Any) -> str:
126
+ """Full pipeline ingestion."""
127
+ from smartmemory_app.storage import ingest
128
+ # storage.ingest() uses 'properties' not 'metadata'
129
+ if "metadata" in kwargs:
130
+ kwargs["properties"] = kwargs.pop("metadata")
131
+ # DIST-LITE-QUIET-1: attribute the write (the /remember skill + MCP memory_ingest
132
+ # surface) so it lands as tier-2 user content, not origin='unknown'. A caller-
133
+ # supplied origin is preserved.
134
+ kwargs.setdefault("origin", "mcp:memory_ingest")
135
+ return ingest(content, memory_type, **kwargs)
136
+
137
+ def recall(self, cwd: str | None = None, top_k: int = 10, **kwargs: Any) -> str:
138
+ """Context-aware recall."""
139
+ from smartmemory_app.storage import recall
140
+ return recall(cwd, top_k)
141
+
142
+ def ingest_structured(self, data: dict, schema: str | None = None, schema_name: str | None = None, **kwargs: Any) -> str:
143
+ """Structured data ingestion."""
144
+ name = schema or schema_name
145
+ return self._mem.ingest_structured(data, schema=name)
146
+
147
+ def ingest_conversation_sync(
148
+ self,
149
+ turns: list,
150
+ session_boundaries: list | None = None,
151
+ conversation_id: str | None = None,
152
+ session_dates: list | None = None,
153
+ turns_per_chunk: int = 15,
154
+ max_chunk_chars: int = 12000,
155
+ max_concurrent: int = 4,
156
+ **kwargs: Any,
157
+ ) -> dict[str, Any]:
158
+ """Conversation bulk ingestion via local SmartMemory (RLM-1g)."""
159
+ from dataclasses import asdict
160
+
161
+ response = self._mem.ingest_conversation_sync(
162
+ turns,
163
+ session_boundaries=session_boundaries,
164
+ conversation_id=conversation_id,
165
+ session_dates=session_dates,
166
+ turns_per_chunk=turns_per_chunk,
167
+ max_chunk_chars=max_chunk_chars,
168
+ max_concurrent=max_concurrent,
169
+ )
170
+ return asdict(response) if hasattr(response, "__dataclass_fields__") else response
171
+
172
+ def read_around(
173
+ self,
174
+ item_id: str,
175
+ char_budget: int = 20000,
176
+ before_ratio: float = 0.3,
177
+ after_ratio: float = 0.7,
178
+ cursor: dict | None = None,
179
+ **kwargs: Any,
180
+ ) -> dict[str, Any]:
181
+ """Auto-centered conversation read via local SmartMemory (CORE-RECALL-CENTERED-1 P2).
182
+
183
+ Returns a char-budgeted asymmetric window of the conversation chunks
184
+ surrounding ``item_id``. Hard-truncates (raise_on_overflow=False) rather
185
+ than raising, so the MCP tool always returns a window.
186
+ """
187
+ return self._mem.read_around(
188
+ item_id,
189
+ char_budget=char_budget,
190
+ before_ratio=before_ratio,
191
+ after_ratio=after_ratio,
192
+ cursor=cursor,
193
+ raise_on_overflow=False,
194
+ )
195
+
196
+ # -- Listing & Stats --
197
+
198
+ def list_memories(self, limit: int = 100, offset: int = 0, **kwargs: Any) -> list[MemoryResult]:
199
+ """List memories with pagination."""
200
+ return normalize_items(self._mem.list_memories(limit=limit, offset=offset))
201
+
202
+ def clear_user_memories(self, confirm: bool = False, **kwargs: Any) -> str:
203
+ """Clear all user memories."""
204
+ if not confirm:
205
+ return "Pass confirm=True to clear all memories."
206
+ self._mem.clear_user_memories()
207
+ return "All memories cleared."
208
+
209
+ def get_all_items_debug(self, **kwargs: Any) -> dict:
210
+ """Get debug stats."""
211
+ return self._mem.get_all_items_debug()
212
+
213
+ def stats(self, **kwargs: Any) -> dict:
214
+ """Memory statistics."""
215
+ return self.get_all_items_debug(**kwargs)
216
+
217
+ # -- Evolution --
218
+
219
+ def run_evolution_cycle(self, **kwargs: Any) -> dict:
220
+ """Trigger evolution cycle."""
221
+ return self._mem.run_evolution_cycle(**kwargs)
222
+
223
+ # CORE-MEMORY-DYNAMICS-1 M1b: commit_working_to_* removed — core façades
224
+ # are gone, ConsolidationRouter routes at ingest. Use add()/ingest() with
225
+ # memory_type="pending" instead.
226
+
227
+ def run_evolver(self, evolver_class: Any, **kwargs: Any) -> dict:
228
+ """Run a specific evolver."""
229
+ return self._mem.run_evolver(evolver_class, **kwargs)
230
+
231
+ def run_clustering(self, **kwargs: Any) -> dict:
232
+ """Run clustering."""
233
+ return self._mem.run_clustering(**kwargs)
234
+
235
+ # -- Insight --
236
+
237
+ def reflect(self, **kwargs: Any) -> str:
238
+ """Reflective analysis."""
239
+ return self._mem.reflect(**kwargs)
240
+
241
+ def summary(self, **kwargs: Any) -> dict:
242
+ """Memory summary."""
243
+ return self._mem.summary(**kwargs)
244
+
245
+ def orphaned_notes(self, **kwargs: Any) -> list[MemoryResult]:
246
+ """Find orphaned notes."""
247
+ return normalize_items(self._mem.orphaned_notes(**kwargs))
248
+
249
+ def find_old_notes(self, days: int = 90, **kwargs: Any) -> list[MemoryResult]:
250
+ """Find notes older than the given number of days."""
251
+ return normalize_items(self._mem.find_old_notes(days, **kwargs))
252
+
253
+ def personalize(self, user_id: str = "mcp-user", traits: dict | None = None, preferences: dict | None = None, **kwargs: Any) -> str:
254
+ """Personalize memory system."""
255
+ return self._mem.personalize(user_id=user_id, traits=traits or {}, preferences=preferences or {}, **kwargs)
256
+
257
+ def update_from_feedback(self, feedback: dict | None = None, memory_type: str = "semantic", **kwargs: Any) -> str:
258
+ """Update from user feedback."""
259
+ return self._mem.update_from_feedback(feedback=feedback or {}, memory_type=memory_type, **kwargs)
260
+
261
+ def ground(self, item_id: str, **kwargs: Any) -> dict:
262
+ """Ground a memory item."""
263
+ return self._mem.ground(item_id=item_id, **kwargs)
264
+
265
+ # -- Graph --
266
+
267
+ def link(self, source_id: str, target_id: str, link_type: str = "RELATES_TO", **kwargs: Any) -> str:
268
+ """Link two memories."""
269
+ return self._mem.link(source_id, target_id, link_type=link_type)
270
+
271
+ def add_edge(self, source_id: str, target_id: str, relation_type: str, **kwargs: Any) -> str:
272
+ """Add a graph edge."""
273
+ return self._mem.add_edge(source_id, target_id, relation_type=relation_type, **kwargs)
274
+
275
+ def get_links(self, item_id: str, **kwargs: Any) -> list[MemoryResult]:
276
+ """Get links for an item."""
277
+ return normalize_items(self._mem.get_links(item_id))
278
+
279
+ def get_neighbors(self, item_id: str, **kwargs: Any) -> dict:
280
+ """Get graph neighbors."""
281
+ return self._mem.get_neighbors(item_id)
282
+
283
+ def find_shortest_path(self, start_id: str, end_id: str, **kwargs: Any) -> list:
284
+ """Find shortest path between two items."""
285
+ return self._mem.find_shortest_path(start_id, end_id, **kwargs)
286
+
287
+ # -- Auth (no-ops for local) --
288
+
289
+ def login(self, api_key: str, **kwargs: Any) -> str:
290
+ """No-op in local mode."""
291
+ return "Local mode — no authentication required."
292
+
293
+ def whoami(self, **kwargs: Any) -> str:
294
+ """Local mode session info."""
295
+ return "Local mode — single user."
296
+
297
+ def switch_team(self, team_id: str, **kwargs: Any) -> str:
298
+ """No-op in local mode."""
299
+ return "Local mode — teams not applicable."
300
+
301
+ # -- Retrieval feedback (SELF-IMPROVE-6) --
302
+
303
+ def submit_feedback(self, search_session_id: str, result_used: list[str], **kwargs: Any) -> dict:
304
+ """Emit result-selection feedback via core retrieval_tracking (local mode).
305
+
306
+ Uses the shown_ids captured from the most recent search() call to provide
307
+ accurate selection rate data (not just result_used == result_shown).
308
+ """
309
+ try:
310
+ from smartmemory.observability.retrieval_tracking import emit_result_feedback
311
+
312
+ # Use shown_ids from the search that produced these results
313
+ shown_ids = getattr(self, "_last_shown_ids", None) or []
314
+ if not shown_ids:
315
+ log.warning("submit_feedback: no prior search() call — shown_ids unknown, skipping")
316
+ return {"status": "skipped", "reason": "no prior search context"}
317
+
318
+ emit_result_feedback(
319
+ query_hash="",
320
+ result_used=result_used,
321
+ result_shown=shown_ids,
322
+ workspace_id="default",
323
+ search_session_id=search_session_id,
324
+ )
325
+ return {"status": "ok", "search_session_id": search_session_id, "result_used_count": len(result_used)}
326
+ except Exception as exc:
327
+ return {"error": str(exc)}