memem 2.9.5__py3-none-any.whl

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.
memem/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """memem — persistent, self-evolving memory for Claude Code."""
2
+
3
+ __version__ = "2.9.5"
memem/assembly.py ADDED
@@ -0,0 +1,104 @@
1
+ """Explicit assembly projection.
2
+
3
+ context_assemble is the secondary path for building a structured context
4
+ briefing. After m4, it calls the active slice engine 1-2 times, merges the
5
+ resulting slices into a composite "assembled" MemorySlice, and renders it
6
+ via render_slice_markdown.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from memem.models import _normalize_scope_id, now_iso
13
+
14
+ log = logging.getLogger("memem-assembly")
15
+
16
+ # Threshold: if the primary slice has fewer items than this, augment with general scope.
17
+ _SPARSE_THRESHOLD = 5
18
+
19
+
20
+ def _merge_slices(sub_slices: "list[dict]", query: str, project: str) -> "dict":
21
+ """Fold N item lists into one composite slice dict."""
22
+ from memem.recall import _layer_summary_from_items, _stable_id
23
+
24
+ seen_ids: set[str] = set()
25
+ merged_items: list[dict] = []
26
+
27
+ for sub in sub_slices:
28
+ for item in sub.get("items", []):
29
+ item_id = item.get("id", "")
30
+ if item_id and item_id in seen_ids:
31
+ continue
32
+ if item_id:
33
+ seen_ids.add(item_id)
34
+ merged_items.append(item)
35
+
36
+ layer_summary = _layer_summary_from_items(merged_items)
37
+
38
+ n_subs = len(sub_slices)
39
+ strategy = "primary-only" if n_subs == 1 else "primary+general-augmentation"
40
+
41
+ return {
42
+ "slice_id": _stable_id("assembled", {"query": query, "project": project, "generated_at": now_iso()}),
43
+ "scope_id": project,
44
+ "query": query,
45
+ "generated_at": now_iso(),
46
+ "slice_kind": "assembled",
47
+ "items": merged_items,
48
+ "layer_summary": layer_summary,
49
+ "sub_slices": sub_slices,
50
+ "composition_strategy": strategy,
51
+ }
52
+
53
+
54
+ def context_assemble(query: str, project: str = "default") -> str:
55
+ """Assemble a composite briefing using the v2.0.0 recall pipeline.
56
+
57
+ Calls memory_search for the active project scope, and optionally augments
58
+ with general scope results when the primary result is sparse. Renders via
59
+ the inline recall markdown renderer.
60
+ """
61
+ from memem.recall import (
62
+ _memory_to_item,
63
+ _render_recall_markdown,
64
+ _search_memories,
65
+ _stable_id,
66
+ )
67
+
68
+ normalized = _normalize_scope_id(project)
69
+
70
+ # Primary slice: search active project scope
71
+ primary_mems = _search_memories(
72
+ query, scope_id=normalized, limit=10, record_access=False, expand_links=False
73
+ )
74
+ primary_items = [_memory_to_item(m, include_snippet=True) for m in primary_mems]
75
+ primary_as_sub: dict = {
76
+ "scope_id": normalized,
77
+ "slice_id": _stable_id("assembled-primary", {"query": query, "project": normalized}),
78
+ "items": primary_items,
79
+ }
80
+
81
+ sub_slices = [primary_as_sub]
82
+
83
+ # Cross-project augmentation when primary is sparse
84
+ if len(primary_items) < _SPARSE_THRESHOLD and normalized != "general":
85
+ general_mems = _search_memories(
86
+ query, scope_id="general", limit=10, record_access=False, expand_links=False
87
+ )
88
+ general_items = [_memory_to_item(m, include_snippet=True) for m in general_mems]
89
+ if general_items:
90
+ sub_slices.append({
91
+ "scope_id": "general",
92
+ "slice_id": _stable_id("assembled-general", {"query": query}),
93
+ "items": general_items,
94
+ })
95
+
96
+ composite = _merge_slices(sub_slices, query=query, project=normalized)
97
+
98
+ # Early return if nothing was assembled
99
+ if not composite.get("items"):
100
+ return ""
101
+
102
+ composite["slice_kind"] = "search"
103
+ composite["query"] = query
104
+ return _render_recall_markdown(composite)
memem/capabilities.py ADDED
@@ -0,0 +1,153 @@
1
+ """Runtime capability detection + serialization.
2
+
3
+ memem writes a small JSON file at ``~/.memem/.capabilities`` during the
4
+ bootstrap shim (bootstrap.sh) and on every ``--doctor`` invocation.
5
+
6
+ The capabilities file is written by ``write_capabilities()`` (called by
7
+ --doctor/bootstrap) and is used by ``pretty_report()`` for diagnostic output.
8
+ The package itself does NOT read this file at runtime — capability checks are
9
+ done live (e.g. shutil.which) at the point of use.
10
+
11
+ Schema (v1)::
12
+
13
+ {
14
+ "schema_version": 1,
15
+ "updated_at": "2026-04-14T12:34:56+00:00",
16
+ "python_version": "3.11.6",
17
+ "mcp": true,
18
+ "claude_cli": true,
19
+ "writable_state_dir": true,
20
+ "writable_vault": true,
21
+ "uv": true,
22
+ "notes": []
23
+ }
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ import os
31
+ import shutil
32
+ import subprocess
33
+ import sys
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ from memem.models import MEMEM_DIR, OBSIDIAN_MEMORIES_DIR, now_iso
38
+
39
+ log = logging.getLogger("memem-capabilities")
40
+
41
+ CAPABILITIES_FILE = MEMEM_DIR / ".capabilities"
42
+ SCHEMA_VERSION = 1
43
+
44
+
45
+ def _can_write(path: Path) -> bool:
46
+ """Probe whether we can actually create and remove a file under ``path``."""
47
+ try:
48
+ path.mkdir(parents=True, exist_ok=True)
49
+ canary = path / ".memem-write-check"
50
+ canary.write_text("ok")
51
+ canary.unlink(missing_ok=True)
52
+ return True
53
+ except OSError:
54
+ return False
55
+
56
+
57
+ def _mcp_importable() -> bool:
58
+ try:
59
+ import importlib.util
60
+ return importlib.util.find_spec("mcp") is not None
61
+ except Exception:
62
+ return False
63
+
64
+
65
+ def _claude_cli_available() -> bool:
66
+ if shutil.which("claude") is None:
67
+ return False
68
+ # Extra sanity: make sure it actually runs (some installs are stale symlinks).
69
+ try:
70
+ result = subprocess.run(
71
+ ["claude", "--version"],
72
+ capture_output=True, text=True, timeout=3,
73
+ )
74
+ return result.returncode == 0
75
+ except (OSError, subprocess.TimeoutExpired):
76
+ return False
77
+
78
+
79
+ def _uv_available() -> bool:
80
+ return shutil.which("uv") is not None
81
+
82
+
83
+ def detect_capabilities() -> dict[str, Any]:
84
+ """Run every probe and return the capabilities dict. Does not write to disk."""
85
+ caps: dict[str, Any] = {
86
+ "schema_version": SCHEMA_VERSION,
87
+ "updated_at": now_iso(),
88
+ "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
89
+ "mcp": _mcp_importable(),
90
+ "claude_cli": _claude_cli_available(),
91
+ "writable_state_dir": _can_write(MEMEM_DIR),
92
+ "writable_vault": _can_write(OBSIDIAN_MEMORIES_DIR),
93
+ "uv": _uv_available(),
94
+ "notes": [],
95
+ }
96
+ return caps
97
+
98
+
99
+ def write_capabilities(caps: dict[str, Any] | None = None) -> dict[str, Any]:
100
+ """Detect (if not supplied) and atomically persist to ``~/.memem/.capabilities``."""
101
+ if caps is None:
102
+ caps = detect_capabilities()
103
+ MEMEM_DIR.mkdir(parents=True, exist_ok=True)
104
+ tmp = CAPABILITIES_FILE.with_suffix(".tmp")
105
+ with open(tmp, "w") as fh:
106
+ fh.write(json.dumps(caps, indent=2, sort_keys=True))
107
+ fh.flush()
108
+ os.fsync(fh.fileno())
109
+ os.replace(tmp, CAPABILITIES_FILE)
110
+ return caps
111
+
112
+
113
+ def pretty_report(caps: dict[str, Any] | None = None) -> str:
114
+ """Human-readable multi-line report. Used by ``--doctor``."""
115
+ if caps is None:
116
+ caps = detect_capabilities()
117
+ lines = [
118
+ "memem Doctor",
119
+ "=" * 40,
120
+ f" Python version : {caps.get('python_version', '?')}",
121
+ f" mcp importable : {'yes' if caps.get('mcp') else 'NO — pip install mcp'}",
122
+ f" claude CLI on PATH : {'yes' if caps.get('claude_cli') else 'NO — Haiku assembly disabled (degraded)'}",
123
+ f" uv available : {'yes' if caps.get('uv') else 'no (bootstrap.sh will install)'}",
124
+ f" state dir writable : {'yes' if caps.get('writable_state_dir') else 'NO — set MEMEM_DIR env var'}",
125
+ f" vault writable : {'yes' if caps.get('writable_vault') else 'NO — set MEMEM_OBSIDIAN_VAULT env var'}",
126
+ f" updated_at : {caps.get('updated_at', '?')}",
127
+ "=" * 40,
128
+ ]
129
+ if caps.get("notes"):
130
+ lines.append("Notes:")
131
+ for note in caps["notes"]:
132
+ lines.append(f" - {note}")
133
+ lines.append("=" * 40)
134
+
135
+ blockers = []
136
+ if not caps.get("mcp"):
137
+ blockers.append("mcp package missing — MCP server cannot start")
138
+ if not caps.get("writable_state_dir"):
139
+ blockers.append("~/.memem is not writable")
140
+ if not caps.get("writable_vault"):
141
+ blockers.append("obsidian vault directory is not writable")
142
+
143
+ if blockers:
144
+ lines.append("BLOCKERS:")
145
+ for b in blockers:
146
+ lines.append(f" ✗ {b}")
147
+ lines.append("=" * 40)
148
+ lines.append("RESULT: FAILING — fix blockers above before first use.")
149
+ else:
150
+ degraded = not caps.get("claude_cli", False)
151
+ status = "DEGRADED (FTS-only recall, no Haiku assembly)" if degraded else "HEALTHY"
152
+ lines.append(f"RESULT: {status}")
153
+ return "\n".join(lines)