graph-mind-memory 0.4.0__tar.gz → 0.5.0__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 (33) hide show
  1. {graph_mind_memory-0.4.0/graph_mind_memory.egg-info → graph_mind_memory-0.5.0}/PKG-INFO +9 -4
  2. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/README.md +7 -3
  3. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/brain_log.py +68 -5
  4. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/forget.py +10 -6
  5. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0/graph_mind_memory.egg-info}/PKG-INFO +9 -4
  6. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/requires.txt +1 -0
  7. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/install.py +57 -10
  8. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/pyproject.toml +2 -1
  9. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/LICENSE +0 -0
  10. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/associative_memory.py +0 -0
  11. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/automatic_capture.py +0 -0
  12. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/automatic_capture_cli.py +0 -0
  13. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/clock.py +0 -0
  14. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/coding_memory.py +0 -0
  15. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/conversation_memory.py +0 -0
  16. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/development_memory.py +0 -0
  17. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/development_paths.py +0 -0
  18. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/embedding_warmup.py +0 -0
  19. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/entity_timeline.py +0 -0
  20. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_mcp_server.py +0 -0
  21. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
  22. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
  23. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/entry_points.txt +0 -0
  24. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/top_level.txt +0 -0
  25. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/local_brain.py +0 -0
  26. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/local_embedder.py +0 -0
  27. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/semantic_llm_extractor.py +0 -0
  28. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/semantic_memory.py +0 -0
  29. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/semantic_recall.py +0 -0
  30. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/setup.cfg +0 -0
  31. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/temporal_resolver.py +0 -0
  32. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/universal_personal_memory.py +0 -0
  33. {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/vector_cache.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graph-mind-memory
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Local-first memory for AI coding assistants: verbatim, on your own PC, no LLM calls at write time.
5
5
  Author-email: Yohan Ko <goyohan0611@gmail.com>
6
6
  License-Expression: AGPL-3.0-only
@@ -20,6 +20,7 @@ Requires-Dist: tokenizers>=0.15
20
20
  Requires-Dist: huggingface_hub>=0.20
21
21
  Requires-Dist: pgserver; python_version < "3.13"
22
22
  Requires-Dist: psycopg[binary]
23
+ Requires-Dist: cryptography
23
24
  Requires-Dist: psutil
24
25
  Requires-Dist: fasteners
25
26
  Dynamic: license-file
@@ -142,7 +143,8 @@ The installer:
142
143
 
143
144
  - installs the packages (no PyTorch: the embedding model runs on ONNX Runtime);
144
145
  - registers the MCP server with every app it finds: Claude Code, Claude Desktop (including the
145
- Microsoft Store build) and Codex;
146
+ Microsoft Store build), Codex, Cursor, Windsurf, Gemini CLI, VS Code (Copilot), Cline and
147
+ LM Studio;
146
148
  - starts the capture service at login (Windows Startup folder, a macOS LaunchAgent, or an XDG
147
149
  autostart entry on Linux);
148
150
  - downloads the embedding model (about 470 MB, once).
@@ -169,6 +171,7 @@ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `
169
171
  | Claude Code: terminal, VS Code, Claude desktop's Code tab | every turn |
170
172
  | Claude desktop: Cowork | every turn |
171
173
  | Claude desktop: chat | what the model saves with `brain_remember` |
174
+ | Cursor, Windsurf, Gemini CLI, VS Code, Cline, LM Studio | what the model saves with `brain_remember` (memory tools connected; their transcripts are not read yet) |
172
175
 
173
176
  Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
174
177
  private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
@@ -232,8 +235,10 @@ PCs reach it over the local network in the office, or over [Tailscale](https://t
232
235
  anywhere. Each connection tries the addresses in turn and uses the first that answers.
233
236
 
234
237
  Other PCs log in with a generated password, as a role that can reach only the memory database. The
235
- Windows firewall rule admits only the local network and Tailscale. The code contains the password:
236
- do not post it publicly.
238
+ Windows firewall rule admits only the local network and Tailscale. Every line of memory is
239
+ encrypted (AES with an HMAC) under a key derived from that password before it leaves a PC, so the
240
+ network and the hub's own database carry only ciphertext. The code contains the password: do not
241
+ post it publicly.
237
242
 
238
243
  A synced folder also works. Tell the AI *"use my Google Drive's Graph-MIND folder as my memory"* on
239
244
  each PC.
@@ -116,7 +116,8 @@ The installer:
116
116
 
117
117
  - installs the packages (no PyTorch: the embedding model runs on ONNX Runtime);
118
118
  - registers the MCP server with every app it finds: Claude Code, Claude Desktop (including the
119
- Microsoft Store build) and Codex;
119
+ Microsoft Store build), Codex, Cursor, Windsurf, Gemini CLI, VS Code (Copilot), Cline and
120
+ LM Studio;
120
121
  - starts the capture service at login (Windows Startup folder, a macOS LaunchAgent, or an XDG
121
122
  autostart entry on Linux);
122
123
  - downloads the embedding model (about 470 MB, once).
@@ -143,6 +144,7 @@ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `
143
144
  | Claude Code: terminal, VS Code, Claude desktop's Code tab | every turn |
144
145
  | Claude desktop: Cowork | every turn |
145
146
  | Claude desktop: chat | what the model saves with `brain_remember` |
147
+ | Cursor, Windsurf, Gemini CLI, VS Code, Cline, LM Studio | what the model saves with `brain_remember` (memory tools connected; their transcripts are not read yet) |
146
148
 
147
149
  Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
148
150
  private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
@@ -206,8 +208,10 @@ PCs reach it over the local network in the office, or over [Tailscale](https://t
206
208
  anywhere. Each connection tries the addresses in turn and uses the first that answers.
207
209
 
208
210
  Other PCs log in with a generated password, as a role that can reach only the memory database. The
209
- Windows firewall rule admits only the local network and Tailscale. The code contains the password:
210
- do not post it publicly.
211
+ Windows firewall rule admits only the local network and Tailscale. Every line of memory is
212
+ encrypted (AES with an HMAC) under a key derived from that password before it leaves a PC, so the
213
+ network and the hub's own database carry only ciphertext. The code contains the password: do not
214
+ post it publicly.
211
215
 
212
216
  A synced folder also works. Tell the AI *"use my Google Drive's Graph-MIND folder as my memory"* on
213
217
  each PC.
@@ -145,6 +145,7 @@ def share(open_firewall: bool = True) -> dict:
145
145
  local_postgres() # first: it explains a missing pgserver
146
146
  from pgserver._commands import pg_ctl
147
147
  password = _config().get("share_password") or secrets.token_urlsafe(18)
148
+ _save({"share_password": password}) # before sealing: the line key derives from it
148
149
  data = _pg_data()
149
150
  with psycopg.connect(_admin_uri(), autocommit=True) as admin:
150
151
  # token_urlsafe is [A-Za-z0-9_-]: nothing in it can close the quoted literal
@@ -160,11 +161,11 @@ def share(open_firewall: bool = True) -> dict:
160
161
  pg_ctl(["-w", "-m", "fast", "restart"] if restart else ["reload"], pgdata=data, timeout=60,
161
162
  **_DETACHED)
162
163
  set_folder("local")
163
- _save({"share_password": password})
164
+ sealed = _seal_existing(local_postgres())
164
165
  hosts = _addresses()
165
166
  code = CODE_PREFIX + base64.urlsafe_b64encode(json.dumps(
166
167
  {"h": hosts, "p": PORT, "k": password}).encode()).decode().rstrip("=")
167
- return {"connection_code": code, "addresses": hosts, "port": PORT,
168
+ return {"connection_code": code, "addresses": hosts, "port": PORT, "lines_sealed": sealed,
168
169
  "firewall": _firewall() if open_firewall else "not changed"}
169
170
 
170
171
 
@@ -308,7 +309,8 @@ def _shared_ids(shared) -> set:
308
309
  """(kind, id) of every turn and memory the shared log holds, forgotten ones included."""
309
310
  if is_database(shared):
310
311
  with _pg(shared) as connection:
311
- lines = [row[0] for row in connection.execute("SELECT line FROM brain_log")]
312
+ lines = [line for line in (_unseal(shared, row[0]) for row in
313
+ connection.execute("SELECT line FROM brain_log")) if line is not None]
312
314
  else:
313
315
  lines = [line for log in (Path(shared) / "log").glob("*.jsonl")
314
316
  for line in log.read_text(encoding="utf-8", errors="replace").splitlines()]
@@ -390,6 +392,66 @@ def _cursor_db(index: Path) -> sqlite3.Connection:
390
392
  return db
391
393
 
392
394
 
395
+ SEALED = "gme1:"
396
+ _KEYS = {}
397
+
398
+
399
+ def _line_key(url):
400
+ """The key that seals this hub's lines, or None when it has none.
401
+
402
+ The Postgres that pgserver ships is built without SSL, so the connection to a hub is not
403
+ encrypted: on a shared network anyone could read memory off the wire. Each line is sealed
404
+ instead (Fernet: AES with an HMAC) under a key derived from the password in the connection
405
+ code, which every PC that joined holds and nobody else does. The hub stores ciphertext too.
406
+ A joined PC finds the password in its URL; the hub PC in its own config."""
407
+ import urllib.parse
408
+ password = urllib.parse.urlsplit(url).password
409
+ password = urllib.parse.unquote(password) if password else _config().get("share_password")
410
+ if not password:
411
+ return None # a hub never shared: nothing leaves this PC
412
+ if password not in _KEYS:
413
+ from cryptography.fernet import Fernet
414
+ from cryptography.hazmat.primitives import hashes
415
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
416
+ raw = HKDF(algorithm=hashes.SHA256(), length=32, salt=b"graph-mind brain_log",
417
+ info=b"line key v1").derive(password.encode())
418
+ _KEYS[password] = Fernet(base64.urlsafe_b64encode(raw))
419
+ return _KEYS[password]
420
+
421
+
422
+ def _seal(url, lines):
423
+ key = _line_key(url)
424
+ return [SEALED + key.encrypt(line.encode()).decode() if key else line for line in lines]
425
+
426
+
427
+ def _unseal(url, line):
428
+ """The plain line, or None when it cannot be opened with this hub's key."""
429
+ if not line.startswith(SEALED):
430
+ return line # written before sealing began
431
+ key = _line_key(url)
432
+ if key is None:
433
+ return None
434
+ from cryptography.fernet import InvalidToken
435
+ try:
436
+ return key.decrypt(line[len(SEALED):].encode()).decode()
437
+ except InvalidToken:
438
+ return None
439
+
440
+
441
+ def _seal_existing(url) -> int:
442
+ """Seal the lines the hub held before it was shared, so none crosses the network in clear."""
443
+ if _line_key(url) is None:
444
+ return 0
445
+ with _pg(url) as connection:
446
+ rows = connection.execute("SELECT seq, line FROM brain_log WHERE line NOT LIKE %s",
447
+ (SEALED + "%",)).fetchall()
448
+ with connection.cursor() as cursor:
449
+ cursor.executemany("UPDATE brain_log SET line = %s WHERE seq = %s",
450
+ [(_seal(url, [line])[0], seq) for seq, line in rows])
451
+ connection.commit()
452
+ return len(rows)
453
+
454
+
393
455
  def _pg(url):
394
456
  import psycopg
395
457
  connection = psycopg.connect(url, autocommit=False)
@@ -432,7 +494,7 @@ def _pg_append(url, lines) -> int:
432
494
  connection.execute("SELECT pg_advisory_xact_lock(4242)")
433
495
  with connection.cursor() as cursor:
434
496
  cursor.executemany("INSERT INTO brain_log(device, line) VALUES (%s, %s)",
435
- [(device_name(), line) for line in lines])
497
+ [(device_name(), line) for line in _seal(url, lines)])
436
498
  connection.commit()
437
499
  return len(lines)
438
500
 
@@ -459,7 +521,8 @@ def _new_lines(db: sqlite3.Connection, shared) -> list[str]:
459
521
  with db:
460
522
  db.execute("INSERT OR REPLACE INTO brain_log_cursor VALUES(?,?)",
461
523
  (source, rows[-1][0]))
462
- return [line for _, line in rows]
524
+ opened = [_unseal(shared, line) for _, line in rows]
525
+ return [line for line in opened if line is not None]
463
526
  lines = []
464
527
  for log in sorted((Path(shared) / "log").glob("*.jsonl")):
465
528
  row = db.execute("SELECT offset FROM brain_log_cursor WHERE source=?",
@@ -96,13 +96,17 @@ def scrub_shared(shared, ids: dict) -> int:
96
96
  if not keys:
97
97
  return 0
98
98
  if is_database(shared):
99
- with _pg(shared) as connection:
100
- removed = 0
101
- for key in keys:
102
- removed += connection.execute(
103
- "DELETE FROM brain_log WHERE strpos(line, %s) > 0", (key,)).rowcount
99
+ from brain_log import _unseal
100
+ with _pg(shared) as connection: # lines are sealed: open each to find targets
101
+ doomed = []
102
+ for seq, line in connection.execute("SELECT seq, line FROM brain_log").fetchall():
103
+ text = _unseal(shared, line) or ""
104
+ if any(key in text for key in keys) and '"kind": "forget"' not in text:
105
+ doomed.append(seq)
106
+ if doomed:
107
+ connection.execute("DELETE FROM brain_log WHERE seq = ANY(%s)", (doomed,))
104
108
  connection.commit()
105
- return removed
109
+ return len(doomed)
106
110
  removed = 0
107
111
  for log in (Path(shared) / "log").glob("*.jsonl"):
108
112
  data = log.read_bytes()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graph-mind-memory
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Local-first memory for AI coding assistants: verbatim, on your own PC, no LLM calls at write time.
5
5
  Author-email: Yohan Ko <goyohan0611@gmail.com>
6
6
  License-Expression: AGPL-3.0-only
@@ -20,6 +20,7 @@ Requires-Dist: tokenizers>=0.15
20
20
  Requires-Dist: huggingface_hub>=0.20
21
21
  Requires-Dist: pgserver; python_version < "3.13"
22
22
  Requires-Dist: psycopg[binary]
23
+ Requires-Dist: cryptography
23
24
  Requires-Dist: psutil
24
25
  Requires-Dist: fasteners
25
26
  Dynamic: license-file
@@ -142,7 +143,8 @@ The installer:
142
143
 
143
144
  - installs the packages (no PyTorch: the embedding model runs on ONNX Runtime);
144
145
  - registers the MCP server with every app it finds: Claude Code, Claude Desktop (including the
145
- Microsoft Store build) and Codex;
146
+ Microsoft Store build), Codex, Cursor, Windsurf, Gemini CLI, VS Code (Copilot), Cline and
147
+ LM Studio;
146
148
  - starts the capture service at login (Windows Startup folder, a macOS LaunchAgent, or an XDG
147
149
  autostart entry on Linux);
148
150
  - downloads the embedding model (about 470 MB, once).
@@ -169,6 +171,7 @@ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `
169
171
  | Claude Code: terminal, VS Code, Claude desktop's Code tab | every turn |
170
172
  | Claude desktop: Cowork | every turn |
171
173
  | Claude desktop: chat | what the model saves with `brain_remember` |
174
+ | Cursor, Windsurf, Gemini CLI, VS Code, Cline, LM Studio | what the model saves with `brain_remember` (memory tools connected; their transcripts are not read yet) |
172
175
 
173
176
  Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
174
177
  private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
@@ -232,8 +235,10 @@ PCs reach it over the local network in the office, or over [Tailscale](https://t
232
235
  anywhere. Each connection tries the addresses in turn and uses the first that answers.
233
236
 
234
237
  Other PCs log in with a generated password, as a role that can reach only the memory database. The
235
- Windows firewall rule admits only the local network and Tailscale. The code contains the password:
236
- do not post it publicly.
238
+ Windows firewall rule admits only the local network and Tailscale. Every line of memory is
239
+ encrypted (AES with an HMAC) under a key derived from that password before it leaves a PC, so the
240
+ network and the hub's own database carry only ciphertext. The code contains the password: do not
241
+ post it publicly.
237
242
 
238
243
  A synced folder also works. Tell the AI *"use my Google Drive's Graph-MIND folder as my memory"* on
239
244
  each PC.
@@ -4,6 +4,7 @@ onnxruntime>=1.17
4
4
  tokenizers>=0.15
5
5
  huggingface_hub>=0.20
6
6
  psycopg[binary]
7
+ cryptography
7
8
  psutil
8
9
  fasteners
9
10
 
@@ -3,8 +3,8 @@
3
3
  python install.py # this PC keeps its own brain
4
4
  python install.py --join gm1.XXXX # ...and joins the brain another PC shared
5
5
 
6
- It installs the Python packages, registers the memory server with Claude Code, Claude Desktop and
7
- Codex (whichever are installed), starts the capture service at login, and downloads the local
6
+ It installs the Python packages, registers the memory server with every MCP client it finds
7
+ (Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Gemini CLI, VS Code, Cline, LM Studio), starts the capture service at login, and downloads the local
8
8
  embedding model so the first question is not the one that waits for it. Running it again is
9
9
  safe, and after moving this folder it re-points everything at the new place.
10
10
  """
@@ -42,9 +42,9 @@ def packages():
42
42
  " or install Python or this folder under a shorter path, e.g. C:\\graph-mind")
43
43
 
44
44
 
45
- def _json_entry(path: Path, **extra) -> None:
45
+ def _json_entry(path: Path, key: str = "mcpServers", **extra) -> None:
46
46
  settings = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
47
- servers = settings.setdefault("mcpServers", {})
47
+ servers = settings.setdefault(key, {})
48
48
  environment = {**servers.get(NAME, {}).get("env", {}), **ENV} # keep the user's own settings
49
49
  servers[NAME] = {**extra, "command": sys.executable, "args": [str(SERVER)], "env": environment}
50
50
  temporary = path.with_suffix(".graph-mind.tmp")
@@ -85,6 +85,46 @@ def claude_desktop(home: Path) -> list[str]:
85
85
  return done[:1]
86
86
 
87
87
 
88
+ def _vscode_user_dir(home: Path) -> Path:
89
+ if platform.system() == "Windows":
90
+ return Path(os.environ.get("APPDATA", home / "AppData" / "Roaming")) / "Code" / "User"
91
+ if platform.system() == "Darwin":
92
+ return home / "Library" / "Application Support" / "Code" / "User"
93
+ return Path(os.environ.get("XDG_CONFIG_HOME") or home / ".config") / "Code" / "User"
94
+
95
+
96
+ def _other_apps(home: Path) -> list:
97
+ """(name, config file, servers key, extra fields) for MCP clients beyond Claude and Codex.
98
+ An app counts as installed when its own folder exists; its config file is created if not."""
99
+ vscode = _vscode_user_dir(home)
100
+ return [
101
+ ("Cursor", home / ".cursor", home / ".cursor" / "mcp.json", "mcpServers", {}),
102
+ ("Windsurf", home / ".codeium" / "windsurf",
103
+ home / ".codeium" / "windsurf" / "mcp_config.json", "mcpServers", {}),
104
+ ("Gemini CLI", home / ".gemini", home / ".gemini" / "settings.json", "mcpServers", {}),
105
+ ("LM Studio", home / ".lmstudio", home / ".lmstudio" / "mcp.json", "mcpServers", {}),
106
+ ("VS Code", vscode, vscode / "mcp.json", "servers", {"type": "stdio"}),
107
+ ("Cline", vscode / "globalStorage" / "saoudrizwan.claude-dev",
108
+ vscode / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
109
+ "mcpServers", {}),
110
+ ]
111
+
112
+
113
+ def other_apps(home: Path) -> list[str]:
114
+ done = []
115
+ for name, folder, config, key, extra in _other_apps(home):
116
+ if not folder.is_dir():
117
+ continue
118
+ try:
119
+ config.parent.mkdir(parents=True, exist_ok=True)
120
+ _json_entry(config, key, **extra)
121
+ done.append(name)
122
+ except ValueError:
123
+ # a settings file with comments (VS Code allows them) is not JSON: leave it alone
124
+ step(f"{name}: could not edit {config}; add the server there by hand")
125
+ return done
126
+
127
+
88
128
  def _without_codex_block(text: str) -> str:
89
129
  """The Codex config without Graph-MIND's server table (and its env table); the rest kept."""
90
130
  header = f"[mcp_servers.{NAME}]"
@@ -227,7 +267,8 @@ def main(argv=None):
227
267
  step("installing packages (the first time takes a few minutes)")
228
268
  packages()
229
269
  home = Path.home()
230
- apps = [name for name in [claude_code(home), *claude_desktop(home), codex(home)] if name]
270
+ apps = [name for name in [claude_code(home), *claude_desktop(home), codex(home),
271
+ *other_apps(home)] if name]
231
272
  step("memory connected to: " + (", ".join(apps) if apps else
232
273
  "no AI app found yet; install one and run this again"))
233
274
  step(capture_service(start=not args.no_start))
@@ -255,17 +296,23 @@ def uninstall(argv=None):
255
296
  claude = shutil.which("claude")
256
297
  if claude:
257
298
  subprocess.run([claude, "mcp", "remove", "-s", "user", NAME], capture_output=True)
258
- for config in [home / ".claude.json"] + [r / "claude_desktop_config.json"
259
- for r in _desktop_roots(home)]:
299
+ targets = ([("Claude Code", home / ".claude.json", "mcpServers")]
300
+ + [("Claude Desktop", r / "claude_desktop_config.json", "mcpServers")
301
+ for r in _desktop_roots(home)]
302
+ + [(name, config, key) for name, _, config, key, _ in _other_apps(home)])
303
+ for label, config, key in targets:
260
304
  if not config.exists():
261
305
  continue
262
- settings = json.loads(config.read_text(encoding="utf-8"))
263
- if settings.get("mcpServers", {}).pop(NAME, None) is not None:
306
+ try:
307
+ settings = json.loads(config.read_text(encoding="utf-8"))
308
+ except ValueError:
309
+ continue
310
+ if settings.get(key, {}).pop(NAME, None) is not None:
264
311
  temporary = config.with_suffix(".graph-mind.tmp")
265
312
  temporary.write_text(json.dumps(settings, ensure_ascii=False, indent=2),
266
313
  encoding="utf-8")
267
314
  os.replace(temporary, config)
268
- removed.append("Claude Code" if config.name == ".claude.json" else "Claude Desktop")
315
+ removed.append(label)
269
316
  codex_config = home / ".codex" / "config.toml"
270
317
  if codex_config.exists():
271
318
  text = codex_config.read_text(encoding="utf-8")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graph-mind-memory"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "Local-first memory for AI coding assistants: verbatim, on your own PC, no LLM calls at write time."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -27,6 +27,7 @@ dependencies = [
27
27
  # brain (psycopg) and everything else work on any supported Python.
28
28
  "pgserver; python_version < '3.13'",
29
29
  "psycopg[binary]",
30
+ "cryptography", # seals lines on a shared hub
30
31
  "psutil",
31
32
  "fasteners",
32
33
  ]