yourmemory 1.2.1__tar.gz → 1.2.3__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 (35) hide show
  1. {yourmemory-1.2.1/yourmemory.egg-info → yourmemory-1.2.3}/PKG-INFO +24 -11
  2. {yourmemory-1.2.1 → yourmemory-1.2.3}/README.md +21 -8
  3. {yourmemory-1.2.1 → yourmemory-1.2.3}/memory_mcp.py +34 -0
  4. {yourmemory-1.2.1 → yourmemory-1.2.3}/pyproject.toml +4 -3
  5. yourmemory-1.2.3/src/services/extract.py +52 -0
  6. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/resolve.py +44 -23
  7. yourmemory-1.2.3/src/services/resolve_fallback.py +192 -0
  8. {yourmemory-1.2.1 → yourmemory-1.2.3/yourmemory.egg-info}/PKG-INFO +24 -11
  9. {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/SOURCES.txt +1 -0
  10. {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/entry_points.txt +1 -0
  11. yourmemory-1.2.1/src/services/extract.py +0 -36
  12. {yourmemory-1.2.1 → yourmemory-1.2.3}/LICENSE +0 -0
  13. {yourmemory-1.2.1 → yourmemory-1.2.3}/setup.cfg +0 -0
  14. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/__init__.py +0 -0
  15. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/app.py +0 -0
  16. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/connection.py +0 -0
  17. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/duckdb_schema.sql +0 -0
  18. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/migrate.py +0 -0
  19. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/schema.sql +0 -0
  20. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/sqlite_schema.sql +0 -0
  21. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/jobs/decay_job.py +0 -0
  22. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/__init__.py +0 -0
  23. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/agents.py +0 -0
  24. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/memories.py +0 -0
  25. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/retrieve.py +0 -0
  26. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/__init__.py +0 -0
  27. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/agent_registry.py +0 -0
  28. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/api_keys.py +0 -0
  29. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/decay.py +0 -0
  30. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/embed.py +0 -0
  31. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/extract_fallback.py +0 -0
  32. {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/retrieve.py +0 -0
  33. {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/dependency_links.txt +0 -0
  34. {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/requires.txt +0 -0
  35. {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yourmemory
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native
5
5
  Author-email: Sachit Misra <mishrasachit1@gmail.com>
6
6
  License: Apache License
@@ -163,8 +163,8 @@ License: Apache License
163
163
  See the License for the specific language governing permissions and
164
164
  limitations under the License.
165
165
 
166
- Project-URL: Homepage, https://github.com/sachitrafa/cognitive-ai-memory
167
- Project-URL: Repository, https://github.com/sachitrafa/cognitive-ai-memory
166
+ Project-URL: Homepage, https://github.com/sachitrafa/YourMemory
167
+ Project-URL: Repository, https://github.com/sachitrafa/YourMemory
168
168
  Keywords: mcp,claude,memory,ebbinghaus,ai,sqlite,postgresql
169
169
  Classifier: Programming Language :: Python :: 3
170
170
  Classifier: Programming Language :: Python :: 3.11
@@ -246,6 +246,8 @@ Importance additionally modulates the decay rate within each category. Memories
246
246
 
247
247
  **Zero infrastructure required** — uses DuckDB out of the box. Two commands and you're done.
248
248
 
249
+ Supports **Python 3.11, 3.12, 3.13, and 3.14**.
250
+
249
251
  ### 1. Install
250
252
 
251
253
  ```bash
@@ -254,17 +256,23 @@ pip install yourmemory
254
256
 
255
257
  All dependencies installed automatically. No clone, no Docker, no database setup.
256
258
 
257
- ### 2. Get your config
259
+ ### 2. Run setup (once)
260
+
261
+ ```bash
262
+ yourmemory-setup
263
+ ```
264
+
265
+ Downloads the spaCy language model and initialises the database. Run this once after install.
258
266
 
259
- Run this once to get your exact config:
267
+ ### 3. Get your config
260
268
 
261
269
  ```bash
262
270
  yourmemory-path
263
271
  ```
264
272
 
265
- It prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
273
+ Prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
266
274
 
267
- ### 3. Wire into your AI client
275
+ ### 4. Wire into your AI client
268
276
 
269
277
  The database is created automatically at `~/.yourmemory/memories.duckdb` on first use.
270
278
 
@@ -286,9 +294,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
286
294
 
287
295
  #### Cline (VS Code)
288
296
 
289
- VS Code doesn't inherit your shell PATH, so use the **full path** from `yourmemory-path`.
297
+ VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
298
+
299
+ ```bash
300
+ yourmemory-path
301
+ ```
290
302
 
291
- In Cline → **MCP Servers** → **Edit MCP Settings**:
303
+ Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
292
304
 
293
305
  ```json
294
306
  {
@@ -305,7 +317,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
305
317
  }
306
318
  ```
307
319
 
308
- Run `yourmemory-path` in terminal — it prints the exact config to paste.
320
+ Restart Cline after saving.
309
321
 
310
322
  #### Cursor
311
323
 
@@ -346,7 +358,7 @@ Restart Claude Desktop.
346
358
 
347
359
  YourMemory is a standard stdio MCP server. Works with Claude Code, Claude Desktop, Cline, Cursor, Windsurf, Continue, and Zed. Use the full path from `yourmemory-path` if the client doesn't inherit shell PATH.
348
360
 
349
- ### 4. Add memory instructions to your project
361
+ ### 5. Add memory instructions to your project
350
362
 
351
363
  Copy `sample_CLAUDE.md` into your project root as `CLAUDE.md` and replace:
352
364
  - `YOUR_NAME` — your name (e.g. `Alice`)
@@ -439,6 +451,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
439
451
 
440
452
  - **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
441
453
  - **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
454
+ - **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
442
455
  - **APScheduler** — automatic 24h decay job
443
456
  - **MCP** — Claude integration via Model Context Protocol
444
457
  - **PostgreSQL + pgvector** — optional, for teams / large datasets
@@ -51,6 +51,8 @@ Importance additionally modulates the decay rate within each category. Memories
51
51
 
52
52
  **Zero infrastructure required** — uses DuckDB out of the box. Two commands and you're done.
53
53
 
54
+ Supports **Python 3.11, 3.12, 3.13, and 3.14**.
55
+
54
56
  ### 1. Install
55
57
 
56
58
  ```bash
@@ -59,17 +61,23 @@ pip install yourmemory
59
61
 
60
62
  All dependencies installed automatically. No clone, no Docker, no database setup.
61
63
 
62
- ### 2. Get your config
64
+ ### 2. Run setup (once)
65
+
66
+ ```bash
67
+ yourmemory-setup
68
+ ```
69
+
70
+ Downloads the spaCy language model and initialises the database. Run this once after install.
63
71
 
64
- Run this once to get your exact config:
72
+ ### 3. Get your config
65
73
 
66
74
  ```bash
67
75
  yourmemory-path
68
76
  ```
69
77
 
70
- It prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
78
+ Prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
71
79
 
72
- ### 3. Wire into your AI client
80
+ ### 4. Wire into your AI client
73
81
 
74
82
  The database is created automatically at `~/.yourmemory/memories.duckdb` on first use.
75
83
 
@@ -91,9 +99,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
91
99
 
92
100
  #### Cline (VS Code)
93
101
 
94
- VS Code doesn't inherit your shell PATH, so use the **full path** from `yourmemory-path`.
102
+ VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
103
+
104
+ ```bash
105
+ yourmemory-path
106
+ ```
95
107
 
96
- In Cline → **MCP Servers** → **Edit MCP Settings**:
108
+ Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
97
109
 
98
110
  ```json
99
111
  {
@@ -110,7 +122,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
110
122
  }
111
123
  ```
112
124
 
113
- Run `yourmemory-path` in terminal — it prints the exact config to paste.
125
+ Restart Cline after saving.
114
126
 
115
127
  #### Cursor
116
128
 
@@ -151,7 +163,7 @@ Restart Claude Desktop.
151
163
 
152
164
  YourMemory is a standard stdio MCP server. Works with Claude Code, Claude Desktop, Cline, Cursor, Windsurf, Continue, and Zed. Use the full path from `yourmemory-path` if the client doesn't inherit shell PATH.
153
165
 
154
- ### 4. Add memory instructions to your project
166
+ ### 5. Add memory instructions to your project
155
167
 
156
168
  Copy `sample_CLAUDE.md` into your project root as `CLAUDE.md` and replace:
157
169
  - `YOUR_NAME` — your name (e.g. `Alice`)
@@ -244,6 +256,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
244
256
 
245
257
  - **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
246
258
  - **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
259
+ - **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
247
260
  - **APScheduler** — automatic 24h decay job
248
261
  - **MCP** — Claude integration via Model Context Protocol
249
262
  - **PostgreSQL + pgvector** — optional, for teams / large datasets
@@ -563,6 +563,40 @@ def print_path():
563
563
  print("Paste this into your Cline MCP settings:\n")
564
564
  print(_json.dumps(config, indent=2))
565
565
 
566
+ def setup():
567
+ """Run once after pip install to download the spaCy model."""
568
+ import subprocess
569
+ print("YourMemory setup — installing spaCy language model...")
570
+ result = subprocess.run(
571
+ [sys.executable, "-m", "spacy", "download", "en_core_web_sm"],
572
+ check=False,
573
+ )
574
+ if result.returncode == 0:
575
+ print("✓ spaCy model installed successfully.")
576
+ else:
577
+ # Fallback: install via direct wheel URL
578
+ print("Direct download fallback...")
579
+ result2 = subprocess.run(
580
+ [sys.executable, "-m", "pip", "install",
581
+ "https://github.com/explosion/spacy-models/releases/download/"
582
+ "en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl",
583
+ "--break-system-packages"],
584
+ check=False,
585
+ )
586
+ if result2.returncode == 0:
587
+ print("✓ spaCy model installed successfully.")
588
+ else:
589
+ print("✗ Could not install spaCy model automatically.")
590
+ print(" Run manually: python -m spacy download en_core_web_sm")
591
+ print(" YourMemory will still work using the built-in regex fallback.")
592
+
593
+ # Also run DB migration
594
+ from src.db.migrate import migrate
595
+ migrate()
596
+ print("✓ Database initialised.")
597
+ print("\nSetup complete. Run yourmemory-path to get your MCP config.")
598
+
599
+
566
600
  def run():
567
601
  from src.db.migrate import migrate
568
602
  migrate()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "yourmemory"
7
- version = "1.2.1"
7
+ version = "1.2.3"
8
8
  description = "Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -39,10 +39,11 @@ all = ["yourmemory[postgres,sse]"]
39
39
  [project.scripts]
40
40
  yourmemory = "memory_mcp:run"
41
41
  yourmemory-path = "memory_mcp:print_path"
42
+ yourmemory-setup = "memory_mcp:setup"
42
43
 
43
44
  [project.urls]
44
- Homepage = "https://github.com/sachitrafa/cognitive-ai-memory"
45
- Repository = "https://github.com/sachitrafa/cognitive-ai-memory"
45
+ Homepage = "https://github.com/sachitrafa/YourMemory"
46
+ Repository = "https://github.com/sachitrafa/YourMemory"
46
47
 
47
48
  [tool.setuptools]
48
49
  py-modules = ["memory_mcp"]
@@ -0,0 +1,52 @@
1
+ import re
2
+ import sys
3
+
4
+ _QUESTION_WORDS = {"what", "who", "where", "when", "why", "how", "which", "whose", "whom"}
5
+
6
+ _IMPERATIVE_PATTERNS = [
7
+ r'^(please|use|try|do|don\'t|make|create|add|remove|delete|update)',
8
+ r'^(convert|transform|change|modify|fix|help|show|tell)',
9
+ r'^(install|run|execute|start|stop|restart|configure)',
10
+ ]
11
+
12
+ # Load spaCy if available — falls back to regex if model not installed yet
13
+ # Run `yourmemory-setup` once after pip install to download the model
14
+ _nlp = None
15
+ try:
16
+ import spacy
17
+ _nlp = spacy.load("en_core_web_sm")
18
+ except OSError:
19
+ print(
20
+ "YourMemory: spaCy model not found. Run `yourmemory-setup` once to install it.\n"
21
+ " Falling back to built-in regex categorization.",
22
+ file=sys.stderr,
23
+ )
24
+ except Exception:
25
+ pass
26
+
27
+
28
+ def is_question(text: str) -> bool:
29
+ """Return True if the text is a question — questions are not stored as memories."""
30
+ stripped = text.strip()
31
+ if stripped.endswith("?"):
32
+ return True
33
+ first_word = re.split(r"\s+", stripped.lower())[0]
34
+ return first_word in _QUESTION_WORDS
35
+
36
+
37
+ def categorize(text: str) -> str:
38
+ """
39
+ Classify text as fact or assumption.
40
+ Uses spaCy dependency parse when available, regex heuristics otherwise.
41
+ Run `yourmemory-setup` to enable spaCy.
42
+ """
43
+ if _nlp is not None:
44
+ doc = _nlp(text)
45
+ has_subject = any(tok.dep_ in ("nsubj", "nsubjpass") for tok in doc)
46
+ return "fact" if has_subject else "assumption"
47
+
48
+ text_lower = text.lower().strip()
49
+ for pattern in _IMPERATIVE_PATTERNS:
50
+ if re.match(pattern, text_lower):
51
+ return "assumption"
52
+ return "fact"
@@ -16,17 +16,6 @@ from src.db.connection import get_backend
16
16
  DEDUP_THRESHOLD = 0.65 # below → always new memory
17
17
  REINFORCE_THRESHOLD = 0.85 # at or above → reinforce (near-identical paraphrase)
18
18
 
19
- # Polarity verb antonym pairs (spaCy lemma → antonym lemma)
20
- _ANTONYMS = {
21
- "love": "hate", "hate": "love",
22
- "like": "dislike", "dislike": "like",
23
- "prefer": "avoid", "avoid": "prefer",
24
- "use": "stop", "stop": "use",
25
- "want": "refuse", "refuse": "want",
26
- "enjoy": "dislike",
27
- "start": "stop",
28
- }
29
-
30
19
 
31
20
  def _cosine(a: list, b: list) -> float:
32
21
  import numpy as np
@@ -101,19 +90,50 @@ def find_near_duplicate(user_id: str, embedding: list, conn) -> dict | None:
101
90
  "importance": best[3], "recall_count": best[4], "similarity": sim}
102
91
 
103
92
 
93
+ _POSITIVE_VERBS = {
94
+ "love", "like", "prefer", "enjoy", "use", "want", "start",
95
+ "adopt", "recommend", "favor", "support", "trust", "appreciate",
96
+ }
97
+ _NEGATIVE_VERBS = {
98
+ "hate", "dislike", "avoid", "stop", "refuse", "abandon",
99
+ "reject", "distrust", "dislike", "despise",
100
+ }
101
+
102
+
103
+ def _polarity(doc) -> int:
104
+ """
105
+ Return +1 (positive), -1 (negative), or 0 (neutral) for a doc.
106
+ Uses root verb lemma + negation detection — no sentiment model needed.
107
+ """
108
+ for token in doc:
109
+ # Use both lemma and raw text to handle spaCy lemmatization bugs
110
+ # e.g. "hates" → lemma "hat" (wrong) but text.rstrip("s") → "hate"
111
+ lemma = token.lemma_.lower()
112
+ raw = token.text.lower().rstrip("s") # crude but catches loves/hates/likes/dislikes
113
+ is_negated = any(child.dep_ == "neg" for child in token.children)
114
+
115
+ if lemma in _POSITIVE_VERBS or raw in _POSITIVE_VERBS:
116
+ return -1 if is_negated else +1
117
+ if lemma in _NEGATIVE_VERBS or raw in _NEGATIVE_VERBS:
118
+ return +1 if is_negated else -1
119
+ return 0
120
+
121
+
104
122
  def detect_contradiction(existing_text: str, incoming_text: str) -> bool:
105
123
  """
106
124
  Return True if the incoming text contradicts the existing one.
107
- Uses spaCy lemmas to find polarity verb antonym pairs.
125
+ Detects polarity flip using verb lemmas + negation generalizes beyond
126
+ a fixed antonym list by treating any positive→negative or negative→positive
127
+ shift on the same topic as a contradiction.
108
128
  """
109
- existing_verbs = {tok.lemma_.lower() for tok in _nlp(existing_text) if tok.pos_ == "VERB"}
110
- incoming_verbs = {tok.lemma_.lower() for tok in _nlp(incoming_text) if tok.pos_ == "VERB"}
129
+ if _nlp is None:
130
+ return False
131
+
132
+ existing_pol = _polarity(_nlp(existing_text))
133
+ incoming_pol = _polarity(_nlp(incoming_text))
111
134
 
112
- for verb in existing_verbs:
113
- antonym = _ANTONYMS.get(verb)
114
- if antonym and antonym in incoming_verbs:
115
- return True
116
- return False
135
+ # Both sentences must have clear polarity and they must be opposite
136
+ return existing_pol != 0 and incoming_pol != 0 and existing_pol != incoming_pol
117
137
 
118
138
 
119
139
  def merge_entities(existing_text: str, incoming_text: str) -> str:
@@ -168,13 +188,14 @@ def resolve(user_id: str, content: str, embedding: list, conn) -> dict:
168
188
 
169
189
  sim = match["similarity"]
170
190
 
171
- if sim >= REINFORCE_THRESHOLD:
172
- return {"action": "reinforce", "content": match["content"], "existing": match}
173
-
174
- # DEDUP_THRESHOLD ≤ sim < REINFORCE_THRESHOLD
191
+ # Check contradiction first — even near-identical sentences can be opposites
192
+ # e.g. "dislike JavaScript" vs "love JavaScript" → sim ~0.92 but must replace
175
193
  if detect_contradiction(match["content"], content):
176
194
  return {"action": "replace", "content": content, "existing": match}
177
195
 
196
+ if sim >= REINFORCE_THRESHOLD:
197
+ return {"action": "reinforce", "content": match["content"], "existing": match}
198
+
178
199
  merged = merge_entities(match["content"], content)
179
200
  if merged == match["content"]:
180
201
  # No new entities found — treat as paraphrase
@@ -0,0 +1,192 @@
1
+ """
2
+ Semantic deduplication for POST /memories - Fallback version without spaCy.
3
+
4
+ Detects near-duplicate memories via cosine similarity and applies one of:
5
+ - reinforce : sim ≥ 0.85 — paraphrase, bump recall_count only
6
+ - replace : 0.65–0.85 + contradiction detected — overwrite with incoming
7
+ - merge : 0.65–0.85 + no contradiction — entity-append to existing
8
+ - new : sim < 0.65 — genuinely distinct, plain INSERT
9
+ """
10
+
11
+ import json
12
+ import math
13
+ import re
14
+ from src.db.connection import get_backend
15
+
16
+ DEDUP_THRESHOLD = 0.65 # below → always new memory
17
+ REINFORCE_THRESHOLD = 0.85 # at or above → reinforce (near-identical paraphrase)
18
+
19
+ # Simple contradiction detection patterns (fallback)
20
+ _CONTRADICTION_PATTERNS = [
21
+ (r'\b(love|like|prefer|enjoy)\b', r'\b(hate|dislike|avoid)\b'),
22
+ (r'\b(start|begin|use)\b', r'\b(stop|quit|avoid)\b'),
23
+ (r'\b(want|need)\b', r'\b(refuse|reject)\b'),
24
+ (r'\b(good|great|excellent)\b', r'\b(bad|terrible|awful)\b'),
25
+ (r'\b(yes|true|correct)\b', r'\b(no|false|wrong)\b'),
26
+ ]
27
+
28
+
29
+ def _cosine(a: list, b: list) -> float:
30
+ import numpy as np
31
+ va, vb = np.array(a, dtype=float), np.array(b, dtype=float)
32
+ denom = np.linalg.norm(va) * np.linalg.norm(vb)
33
+ return float(np.dot(va, vb) / denom) if denom else 0.0
34
+
35
+
36
+ def find_near_duplicate(user_id: str, embedding: list, conn) -> dict | None:
37
+ """
38
+ Return the closest existing memory if cosine similarity >= DEDUP_THRESHOLD,
39
+ else None. Uses the caller's open connection.
40
+ """
41
+ backend = get_backend()
42
+
43
+ if backend == "postgres":
44
+ embedding_str = f"[{','.join(str(x) for x in embedding)}]"
45
+ cur = conn.cursor()
46
+ cur.execute("""
47
+ SELECT id, content, category, importance, recall_count,
48
+ 1 - (embedding <=> %s::vector) AS similarity
49
+ FROM memories
50
+ WHERE user_id = %s
51
+ ORDER BY embedding <=> %s::vector
52
+ LIMIT 1
53
+ """, (embedding_str, user_id, embedding_str))
54
+ row = cur.fetchone()
55
+ cur.close()
56
+ if row is None:
57
+ return None
58
+ sim = row[5]
59
+ if sim < DEDUP_THRESHOLD:
60
+ return None
61
+ return {"id": row[0], "content": row[1], "category": row[2],
62
+ "importance": row[3], "recall_count": row[4], "similarity": sim}
63
+
64
+ if backend == "duckdb":
65
+ from src.db.connection import duckdb_row
66
+ cur = conn.execute("""
67
+ SELECT id, content, category, importance, recall_count,
68
+ array_cosine_similarity(embedding, ?::FLOAT[768]) AS similarity
69
+ FROM memories
70
+ WHERE user_id = ?
71
+ ORDER BY similarity DESC
72
+ LIMIT 1
73
+ """, [embedding, user_id])
74
+ row = duckdb_row(cur)
75
+ if row is None or row["similarity"] < DEDUP_THRESHOLD:
76
+ return None
77
+ return row
78
+
79
+ # SQLite: numpy cosine over all user memories
80
+ cur = conn.cursor()
81
+ cur.execute("""
82
+ SELECT id, content, category, importance, recall_count, embedding
83
+ FROM memories WHERE user_id = ?
84
+ """, (user_id,))
85
+ rows = cur.fetchall()
86
+ cur.close()
87
+
88
+ best, sim = None, -1.0
89
+ for row in rows:
90
+ raw = row[5] if isinstance(row, tuple) else row["embedding"]
91
+ if raw is None:
92
+ continue
93
+ s = _cosine(embedding, json.loads(raw))
94
+ if s > sim:
95
+ sim, best = s, row
96
+ if best is None or sim < DEDUP_THRESHOLD:
97
+ return None
98
+ return {"id": best[0], "content": best[1], "category": best[2],
99
+ "importance": best[3], "recall_count": best[4], "similarity": sim}
100
+
101
+
102
+ def detect_contradiction(existing_text: str, incoming_text: str) -> bool:
103
+ """
104
+ Fallback contradiction detection using regex patterns.
105
+ Return True if the incoming text contradicts the existing one.
106
+ """
107
+ existing_lower = existing_text.lower()
108
+ incoming_lower = incoming_text.lower()
109
+
110
+ for positive_pattern, negative_pattern in _CONTRADICTION_PATTERNS:
111
+ # Check if existing has positive and incoming has negative
112
+ if re.search(positive_pattern, existing_lower) and re.search(negative_pattern, incoming_lower):
113
+ return True
114
+ # Check if existing has negative and incoming has positive
115
+ if re.search(negative_pattern, existing_lower) and re.search(positive_pattern, incoming_lower):
116
+ return True
117
+
118
+ return False
119
+
120
+
121
+ def merge_entities(existing_text: str, incoming_text: str) -> str:
122
+ """
123
+ Fallback entity merging using simple heuristics.
124
+ Append capitalized words and quoted strings from incoming that are absent from existing.
125
+ Returns the merged string, or existing_text unchanged if nothing new found.
126
+ """
127
+ existing_lower = existing_text.lower()
128
+
129
+ # Extract potential entities using simple patterns
130
+ candidates = []
131
+
132
+ # Capitalized words (potential proper nouns)
133
+ capitalized_words = re.findall(r'\b[A-Z][a-zA-Z]{2,}\b', incoming_text)
134
+ candidates.extend(capitalized_words)
135
+
136
+ # Quoted strings
137
+ quoted_strings = re.findall(r'"([^"]+)"', incoming_text)
138
+ quoted_strings.extend(re.findall(r"'([^']+)'", incoming_text))
139
+ candidates.extend(quoted_strings)
140
+
141
+ # Technical terms (words with numbers, dots, underscores)
142
+ tech_terms = re.findall(r'\b[a-zA-Z][a-zA-Z0-9._-]*[a-zA-Z0-9]\b', incoming_text)
143
+ candidates.extend([t for t in tech_terms if '.' in t or '_' in t or any(c.isdigit() for c in t)])
144
+
145
+ # Filter out terms already present in existing text
146
+ new_terms = [t for t in candidates if t.lower() not in existing_lower and len(t.strip()) > 2]
147
+
148
+ # Deduplicate while preserving order
149
+ seen, deduped = set(), []
150
+ for t in new_terms:
151
+ if t.lower() not in seen:
152
+ seen.add(t.lower())
153
+ deduped.append(t)
154
+
155
+ if not deduped:
156
+ return existing_text
157
+ if len(deduped) == 1:
158
+ return f"{existing_text} with {deduped[0]}"
159
+ return f"{existing_text} with {', '.join(deduped[:-1])} and {deduped[-1]}"
160
+
161
+
162
+ def resolve(user_id: str, content: str, embedding: list, conn) -> dict:
163
+ """
164
+ Facade: decide what to do with an incoming memory.
165
+
166
+ Returns:
167
+ {
168
+ "action": "new" | "reinforce" | "replace" | "merge",
169
+ "content": str, # final content to store/update
170
+ "existing": dict | None, # matched row if any
171
+ }
172
+ """
173
+ match = find_near_duplicate(user_id, embedding, conn)
174
+
175
+ if match is None:
176
+ return {"action": "new", "content": content, "existing": None}
177
+
178
+ sim = match["similarity"]
179
+
180
+ if sim >= REINFORCE_THRESHOLD:
181
+ return {"action": "reinforce", "content": match["content"], "existing": match}
182
+
183
+ # DEDUP_THRESHOLD ≤ sim < REINFORCE_THRESHOLD
184
+ if detect_contradiction(match["content"], content):
185
+ return {"action": "replace", "content": content, "existing": match}
186
+
187
+ merged = merge_entities(match["content"], content)
188
+ if merged == match["content"]:
189
+ # No new entities found — treat as paraphrase
190
+ return {"action": "reinforce", "content": match["content"], "existing": match}
191
+
192
+ return {"action": "merge", "content": merged, "existing": match}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yourmemory
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native
5
5
  Author-email: Sachit Misra <mishrasachit1@gmail.com>
6
6
  License: Apache License
@@ -163,8 +163,8 @@ License: Apache License
163
163
  See the License for the specific language governing permissions and
164
164
  limitations under the License.
165
165
 
166
- Project-URL: Homepage, https://github.com/sachitrafa/cognitive-ai-memory
167
- Project-URL: Repository, https://github.com/sachitrafa/cognitive-ai-memory
166
+ Project-URL: Homepage, https://github.com/sachitrafa/YourMemory
167
+ Project-URL: Repository, https://github.com/sachitrafa/YourMemory
168
168
  Keywords: mcp,claude,memory,ebbinghaus,ai,sqlite,postgresql
169
169
  Classifier: Programming Language :: Python :: 3
170
170
  Classifier: Programming Language :: Python :: 3.11
@@ -246,6 +246,8 @@ Importance additionally modulates the decay rate within each category. Memories
246
246
 
247
247
  **Zero infrastructure required** — uses DuckDB out of the box. Two commands and you're done.
248
248
 
249
+ Supports **Python 3.11, 3.12, 3.13, and 3.14**.
250
+
249
251
  ### 1. Install
250
252
 
251
253
  ```bash
@@ -254,17 +256,23 @@ pip install yourmemory
254
256
 
255
257
  All dependencies installed automatically. No clone, no Docker, no database setup.
256
258
 
257
- ### 2. Get your config
259
+ ### 2. Run setup (once)
260
+
261
+ ```bash
262
+ yourmemory-setup
263
+ ```
264
+
265
+ Downloads the spaCy language model and initialises the database. Run this once after install.
258
266
 
259
- Run this once to get your exact config:
267
+ ### 3. Get your config
260
268
 
261
269
  ```bash
262
270
  yourmemory-path
263
271
  ```
264
272
 
265
- It prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
273
+ Prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
266
274
 
267
- ### 3. Wire into your AI client
275
+ ### 4. Wire into your AI client
268
276
 
269
277
  The database is created automatically at `~/.yourmemory/memories.duckdb` on first use.
270
278
 
@@ -286,9 +294,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
286
294
 
287
295
  #### Cline (VS Code)
288
296
 
289
- VS Code doesn't inherit your shell PATH, so use the **full path** from `yourmemory-path`.
297
+ VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
298
+
299
+ ```bash
300
+ yourmemory-path
301
+ ```
290
302
 
291
- In Cline → **MCP Servers** → **Edit MCP Settings**:
303
+ Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
292
304
 
293
305
  ```json
294
306
  {
@@ -305,7 +317,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
305
317
  }
306
318
  ```
307
319
 
308
- Run `yourmemory-path` in terminal — it prints the exact config to paste.
320
+ Restart Cline after saving.
309
321
 
310
322
  #### Cursor
311
323
 
@@ -346,7 +358,7 @@ Restart Claude Desktop.
346
358
 
347
359
  YourMemory is a standard stdio MCP server. Works with Claude Code, Claude Desktop, Cline, Cursor, Windsurf, Continue, and Zed. Use the full path from `yourmemory-path` if the client doesn't inherit shell PATH.
348
360
 
349
- ### 4. Add memory instructions to your project
361
+ ### 5. Add memory instructions to your project
350
362
 
351
363
  Copy `sample_CLAUDE.md` into your project root as `CLAUDE.md` and replace:
352
364
  - `YOUR_NAME` — your name (e.g. `Alice`)
@@ -439,6 +451,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
439
451
 
440
452
  - **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
441
453
  - **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
454
+ - **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
442
455
  - **APScheduler** — automatic 24h decay job
443
456
  - **MCP** — Claude integration via Model Context Protocol
444
457
  - **PostgreSQL + pgvector** — optional, for teams / large datasets
@@ -22,6 +22,7 @@ src/services/embed.py
22
22
  src/services/extract.py
23
23
  src/services/extract_fallback.py
24
24
  src/services/resolve.py
25
+ src/services/resolve_fallback.py
25
26
  src/services/retrieve.py
26
27
  yourmemory.egg-info/PKG-INFO
27
28
  yourmemory.egg-info/SOURCES.txt
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
2
  yourmemory = memory_mcp:run
3
3
  yourmemory-path = memory_mcp:print_path
4
+ yourmemory-setup = memory_mcp:setup
@@ -1,36 +0,0 @@
1
- import re
2
- import spacy
3
-
4
- try:
5
- _nlp = spacy.load("en_core_web_sm")
6
- except OSError:
7
- import subprocess, sys
8
- subprocess.run(
9
- [sys.executable, "-m", "pip", "install",
10
- "https://github.com/explosion/spacy-models/releases/download/"
11
- "en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl"],
12
- check=True,
13
- )
14
- _nlp = spacy.load("en_core_web_sm")
15
-
16
- _QUESTION_WORDS = {"what", "who", "where", "when", "why", "how", "which", "whose", "whom"}
17
-
18
-
19
- def is_question(text: str) -> bool:
20
- """Return True if the text is a question — questions are not stored as memories."""
21
- stripped = text.strip()
22
- if stripped.endswith("?"):
23
- return True
24
- first_word = re.split(r"\s+", stripped.lower())[0]
25
- return first_word in _QUESTION_WORDS
26
-
27
-
28
- def categorize(text: str) -> str:
29
- """
30
- Use spaCy dependency parse to classify:
31
- fact — declarative sentence with an explicit subject
32
- assumption — imperative sentence with no subject (command/instruction)
33
- """
34
- doc = _nlp(text)
35
- has_subject = any(tok.dep_ in ("nsubj", "nsubjpass") for tok in doc)
36
- return "fact" if has_subject else "assumption"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes