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.
- {graph_mind_memory-0.4.0/graph_mind_memory.egg-info → graph_mind_memory-0.5.0}/PKG-INFO +9 -4
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/README.md +7 -3
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/brain_log.py +68 -5
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/forget.py +10 -6
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0/graph_mind_memory.egg-info}/PKG-INFO +9 -4
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/requires.txt +1 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/install.py +57 -10
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/pyproject.toml +2 -1
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/LICENSE +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/associative_memory.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/automatic_capture.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/clock.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/coding_memory.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/conversation_memory.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/development_memory.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/development_paths.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/entity_timeline.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_mcp_server.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/entry_points.txt +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/top_level.txt +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/local_brain.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/local_embedder.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/semantic_memory.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/semantic_recall.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/setup.cfg +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/universal_personal_memory.py +0 -0
- {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.
|
|
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
|
|
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.
|
|
236
|
-
|
|
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
|
|
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.
|
|
210
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
236
|
-
|
|
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.
|
|
@@ -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
|
|
7
|
-
Codex
|
|
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(
|
|
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)
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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(
|
|
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.
|
|
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
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{graph_mind_memory-0.4.0 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|