graph-mind-memory 0.3.4__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.3.4/graph_mind_memory.egg-info → graph_mind_memory-0.5.0}/PKG-INFO +22 -16
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/README.md +17 -12
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/brain_log.py +68 -5
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/forget.py +10 -6
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0/graph_mind_memory.egg-info}/PKG-INFO +22 -16
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/requires.txt +4 -3
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/install.py +69 -25
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/local_embedder.py +45 -20
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/pyproject.toml +5 -4
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/LICENSE +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/associative_memory.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/automatic_capture.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/clock.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/coding_memory.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/conversation_memory.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/development_memory.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/development_paths.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/entity_timeline.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/graph_mind_mcp_server.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/entry_points.txt +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/top_level.txt +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/local_brain.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/semantic_memory.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/semantic_recall.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/setup.cfg +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/universal_personal_memory.py +0 -0
- {graph_mind_memory-0.3.4 → 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
|
|
@@ -13,13 +13,14 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: torch<3,>=2.2
|
|
17
16
|
Requires-Dist: mcp<3,>=2
|
|
18
17
|
Requires-Dist: numpy
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: onnxruntime>=1.17
|
|
19
|
+
Requires-Dist: tokenizers>=0.15
|
|
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
|
|
@@ -48,8 +49,9 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
|
|
|
48
49
|
</div>
|
|
49
50
|
|
|
50
51
|
> [!NOTE]
|
|
51
|
-
> **Alpha.**
|
|
52
|
-
>
|
|
52
|
+
> **Alpha.** Windows: used daily, with full end-to-end checks (fresh install, two PCs, real
|
|
53
|
+
> Claude and Codex). macOS and Linux: tests and a clean install from PyPI pass in CI on every
|
|
54
|
+
> release and every day, but nobody has used them day to day yet; consider them experimental.
|
|
53
55
|
|
|
54
56
|
---
|
|
55
57
|
|
|
@@ -125,8 +127,8 @@ graph-mind-install
|
|
|
125
127
|
|
|
126
128
|
If `graph-mind-install` is "not recognized", pip put it in a Scripts folder that is not on your
|
|
127
129
|
PATH (common with the Windows Python install manager). `python -m install` runs the same thing.
|
|
128
|
-
If it stops with
|
|
129
|
-
runtime
|
|
130
|
+
If it stops with a DLL load error, Windows is missing the Microsoft Visual C++ runtime that the
|
|
131
|
+
embedding runtime needs: install [vc_redist.x64.exe](https://aka.ms/vs/17/release/vc_redist.x64.exe),
|
|
130
132
|
restart, and run the installer again.
|
|
131
133
|
|
|
132
134
|
or from source:
|
|
@@ -139,12 +141,13 @@ python install.py
|
|
|
139
141
|
|
|
140
142
|
The installer:
|
|
141
143
|
|
|
142
|
-
- installs the packages (PyTorch
|
|
144
|
+
- installs the packages (no PyTorch: the embedding model runs on ONNX Runtime);
|
|
143
145
|
- registers the MCP server with every app it finds: Claude Code, Claude Desktop (including the
|
|
144
|
-
Microsoft Store build) and
|
|
146
|
+
Microsoft Store build), Codex, Cursor, Windsurf, Gemini CLI, VS Code (Copilot), Cline and
|
|
147
|
+
LM Studio;
|
|
145
148
|
- starts the capture service at login (Windows Startup folder, a macOS LaunchAgent, or an XDG
|
|
146
149
|
autostart entry on Linux);
|
|
147
|
-
- downloads the embedding model.
|
|
150
|
+
- downloads the embedding model (about 470 MB, once).
|
|
148
151
|
|
|
149
152
|
Then restart your AI apps. The server is also listed in the
|
|
150
153
|
[MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.goyohan0611-png/graph-mind`. Running the installer again is safe. Run it again if you move the folder.
|
|
@@ -168,6 +171,7 @@ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `
|
|
|
168
171
|
| Claude Code: terminal, VS Code, Claude desktop's Code tab | every turn |
|
|
169
172
|
| Claude desktop: Cowork | every turn |
|
|
170
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) |
|
|
171
175
|
|
|
172
176
|
Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
|
|
173
177
|
private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
|
|
@@ -231,8 +235,10 @@ PCs reach it over the local network in the office, or over [Tailscale](https://t
|
|
|
231
235
|
anywhere. Each connection tries the addresses in turn and uses the first that answers.
|
|
232
236
|
|
|
233
237
|
Other PCs log in with a generated password, as a role that can reach only the memory database. The
|
|
234
|
-
Windows firewall rule admits only the local network and Tailscale.
|
|
235
|
-
|
|
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.
|
|
236
242
|
|
|
237
243
|
A synced folder also works. Tell the AI *"use my Google Drive's Graph-MIND folder as my memory"* on
|
|
238
244
|
each PC.
|
|
@@ -258,9 +264,9 @@ python -m unittest discover -p "test_*.py"
|
|
|
258
264
|
|
|
259
265
|
## Known limits
|
|
260
266
|
|
|
261
|
-
- **For
|
|
262
|
-
embedding model loads in the background (
|
|
263
|
-
|
|
267
|
+
- **For the first few seconds after an AI app starts, answers use word search only** while the
|
|
268
|
+
embedding model loads in the background (6-9 s on a slow, synced disk; it was 50-70 s before
|
|
269
|
+
0.4). The first answer never waits for it; later answers take well under a second.
|
|
264
270
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
265
271
|
use.
|
|
266
272
|
- **Capture follows each app's transcript format,** which is not a public interface. An app update
|
|
@@ -22,8 +22,9 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
> [!NOTE]
|
|
25
|
-
> **Alpha.**
|
|
26
|
-
>
|
|
25
|
+
> **Alpha.** Windows: used daily, with full end-to-end checks (fresh install, two PCs, real
|
|
26
|
+
> Claude and Codex). macOS and Linux: tests and a clean install from PyPI pass in CI on every
|
|
27
|
+
> release and every day, but nobody has used them day to day yet; consider them experimental.
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
@@ -99,8 +100,8 @@ graph-mind-install
|
|
|
99
100
|
|
|
100
101
|
If `graph-mind-install` is "not recognized", pip put it in a Scripts folder that is not on your
|
|
101
102
|
PATH (common with the Windows Python install manager). `python -m install` runs the same thing.
|
|
102
|
-
If it stops with
|
|
103
|
-
runtime
|
|
103
|
+
If it stops with a DLL load error, Windows is missing the Microsoft Visual C++ runtime that the
|
|
104
|
+
embedding runtime needs: install [vc_redist.x64.exe](https://aka.ms/vs/17/release/vc_redist.x64.exe),
|
|
104
105
|
restart, and run the installer again.
|
|
105
106
|
|
|
106
107
|
or from source:
|
|
@@ -113,12 +114,13 @@ python install.py
|
|
|
113
114
|
|
|
114
115
|
The installer:
|
|
115
116
|
|
|
116
|
-
- installs the packages (PyTorch
|
|
117
|
+
- installs the packages (no PyTorch: the embedding model runs on ONNX Runtime);
|
|
117
118
|
- registers the MCP server with every app it finds: Claude Code, Claude Desktop (including the
|
|
118
|
-
Microsoft Store build) and
|
|
119
|
+
Microsoft Store build), Codex, Cursor, Windsurf, Gemini CLI, VS Code (Copilot), Cline and
|
|
120
|
+
LM Studio;
|
|
119
121
|
- starts the capture service at login (Windows Startup folder, a macOS LaunchAgent, or an XDG
|
|
120
122
|
autostart entry on Linux);
|
|
121
|
-
- downloads the embedding model.
|
|
123
|
+
- downloads the embedding model (about 470 MB, once).
|
|
122
124
|
|
|
123
125
|
Then restart your AI apps. The server is also listed in the
|
|
124
126
|
[MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.goyohan0611-png/graph-mind`. Running the installer again is safe. Run it again if you move the folder.
|
|
@@ -142,6 +144,7 @@ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `
|
|
|
142
144
|
| Claude Code: terminal, VS Code, Claude desktop's Code tab | every turn |
|
|
143
145
|
| Claude desktop: Cowork | every turn |
|
|
144
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) |
|
|
145
148
|
|
|
146
149
|
Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
|
|
147
150
|
private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
|
|
@@ -205,8 +208,10 @@ PCs reach it over the local network in the office, or over [Tailscale](https://t
|
|
|
205
208
|
anywhere. Each connection tries the addresses in turn and uses the first that answers.
|
|
206
209
|
|
|
207
210
|
Other PCs log in with a generated password, as a role that can reach only the memory database. The
|
|
208
|
-
Windows firewall rule admits only the local network and Tailscale.
|
|
209
|
-
|
|
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.
|
|
210
215
|
|
|
211
216
|
A synced folder also works. Tell the AI *"use my Google Drive's Graph-MIND folder as my memory"* on
|
|
212
217
|
each PC.
|
|
@@ -232,9 +237,9 @@ python -m unittest discover -p "test_*.py"
|
|
|
232
237
|
|
|
233
238
|
## Known limits
|
|
234
239
|
|
|
235
|
-
- **For
|
|
236
|
-
embedding model loads in the background (
|
|
237
|
-
|
|
240
|
+
- **For the first few seconds after an AI app starts, answers use word search only** while the
|
|
241
|
+
embedding model loads in the background (6-9 s on a slow, synced disk; it was 50-70 s before
|
|
242
|
+
0.4). The first answer never waits for it; later answers take well under a second.
|
|
238
243
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
239
244
|
use.
|
|
240
245
|
- **Capture follows each app's transcript format,** which is not a public interface. An app update
|
|
@@ -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
|
|
@@ -13,13 +13,14 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: torch<3,>=2.2
|
|
17
16
|
Requires-Dist: mcp<3,>=2
|
|
18
17
|
Requires-Dist: numpy
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: onnxruntime>=1.17
|
|
19
|
+
Requires-Dist: tokenizers>=0.15
|
|
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
|
|
@@ -48,8 +49,9 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
|
|
|
48
49
|
</div>
|
|
49
50
|
|
|
50
51
|
> [!NOTE]
|
|
51
|
-
> **Alpha.**
|
|
52
|
-
>
|
|
52
|
+
> **Alpha.** Windows: used daily, with full end-to-end checks (fresh install, two PCs, real
|
|
53
|
+
> Claude and Codex). macOS and Linux: tests and a clean install from PyPI pass in CI on every
|
|
54
|
+
> release and every day, but nobody has used them day to day yet; consider them experimental.
|
|
53
55
|
|
|
54
56
|
---
|
|
55
57
|
|
|
@@ -125,8 +127,8 @@ graph-mind-install
|
|
|
125
127
|
|
|
126
128
|
If `graph-mind-install` is "not recognized", pip put it in a Scripts folder that is not on your
|
|
127
129
|
PATH (common with the Windows Python install manager). `python -m install` runs the same thing.
|
|
128
|
-
If it stops with
|
|
129
|
-
runtime
|
|
130
|
+
If it stops with a DLL load error, Windows is missing the Microsoft Visual C++ runtime that the
|
|
131
|
+
embedding runtime needs: install [vc_redist.x64.exe](https://aka.ms/vs/17/release/vc_redist.x64.exe),
|
|
130
132
|
restart, and run the installer again.
|
|
131
133
|
|
|
132
134
|
or from source:
|
|
@@ -139,12 +141,13 @@ python install.py
|
|
|
139
141
|
|
|
140
142
|
The installer:
|
|
141
143
|
|
|
142
|
-
- installs the packages (PyTorch
|
|
144
|
+
- installs the packages (no PyTorch: the embedding model runs on ONNX Runtime);
|
|
143
145
|
- registers the MCP server with every app it finds: Claude Code, Claude Desktop (including the
|
|
144
|
-
Microsoft Store build) and
|
|
146
|
+
Microsoft Store build), Codex, Cursor, Windsurf, Gemini CLI, VS Code (Copilot), Cline and
|
|
147
|
+
LM Studio;
|
|
145
148
|
- starts the capture service at login (Windows Startup folder, a macOS LaunchAgent, or an XDG
|
|
146
149
|
autostart entry on Linux);
|
|
147
|
-
- downloads the embedding model.
|
|
150
|
+
- downloads the embedding model (about 470 MB, once).
|
|
148
151
|
|
|
149
152
|
Then restart your AI apps. The server is also listed in the
|
|
150
153
|
[MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.goyohan0611-png/graph-mind`. Running the installer again is safe. Run it again if you move the folder.
|
|
@@ -168,6 +171,7 @@ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `
|
|
|
168
171
|
| Claude Code: terminal, VS Code, Claude desktop's Code tab | every turn |
|
|
169
172
|
| Claude desktop: Cowork | every turn |
|
|
170
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) |
|
|
171
175
|
|
|
172
176
|
Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
|
|
173
177
|
private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
|
|
@@ -231,8 +235,10 @@ PCs reach it over the local network in the office, or over [Tailscale](https://t
|
|
|
231
235
|
anywhere. Each connection tries the addresses in turn and uses the first that answers.
|
|
232
236
|
|
|
233
237
|
Other PCs log in with a generated password, as a role that can reach only the memory database. The
|
|
234
|
-
Windows firewall rule admits only the local network and Tailscale.
|
|
235
|
-
|
|
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.
|
|
236
242
|
|
|
237
243
|
A synced folder also works. Tell the AI *"use my Google Drive's Graph-MIND folder as my memory"* on
|
|
238
244
|
each PC.
|
|
@@ -258,9 +264,9 @@ python -m unittest discover -p "test_*.py"
|
|
|
258
264
|
|
|
259
265
|
## Known limits
|
|
260
266
|
|
|
261
|
-
- **For
|
|
262
|
-
embedding model loads in the background (
|
|
263
|
-
|
|
267
|
+
- **For the first few seconds after an AI app starts, answers use word search only** while the
|
|
268
|
+
embedding model loads in the background (6-9 s on a slow, synced disk; it was 50-70 s before
|
|
269
|
+
0.4). The first answer never waits for it; later answers take well under a second.
|
|
264
270
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
265
271
|
use.
|
|
266
272
|
- **Capture follows each app's transcript format,** which is not a public interface. An app update
|
|
@@ -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}]"
|
|
@@ -180,35 +220,32 @@ def capture_service(start: bool) -> str:
|
|
|
180
220
|
|
|
181
221
|
def embedding_model():
|
|
182
222
|
sys.path.insert(0, str(HERE))
|
|
183
|
-
from local_embedder import LOCAL_MODEL, own_hf_home
|
|
223
|
+
from local_embedder import LOCAL_MODEL, MODEL_FILES, model_paths, own_hf_home
|
|
184
224
|
try:
|
|
185
|
-
|
|
186
|
-
except
|
|
187
|
-
if "
|
|
225
|
+
import onnxruntime # noqa: F401 (a DLL failure here would leave search on words only)
|
|
226
|
+
except ImportError as error:
|
|
227
|
+
if platform.system() != "Windows":
|
|
188
228
|
raise
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
sys.exit(f"\n{error}\n\nPyTorch could not load. Install the Microsoft Visual C++ "
|
|
192
|
-
"Redistributable (x64), restart, and run this again:\n"
|
|
229
|
+
sys.exit(f"\n{error}\n\nThe embedding runtime could not load. Install the Microsoft "
|
|
230
|
+
"Visual C++ Redistributable (x64), restart, and run this again:\n"
|
|
193
231
|
" https://aka.ms/vs/17/release/vc_redist.x64.exe\n"
|
|
194
232
|
"The apps are already connected; until then recall works by word search only.")
|
|
195
|
-
|
|
196
233
|
try:
|
|
197
|
-
|
|
198
|
-
AutoModel.from_pretrained(LOCAL_MODEL)
|
|
234
|
+
model_paths()
|
|
199
235
|
except OSError as error:
|
|
200
236
|
# ~/.cache refused (access denied): move Hugging Face's whole home under ~/.graph-mind,
|
|
201
237
|
# where every Graph-MIND process looks from now on (local_embedder.use_own_hf_home)
|
|
202
|
-
if "PermissionError" not in str(error) and not isinstance(error.__cause__, PermissionError)
|
|
238
|
+
if "PermissionError" not in str(error) and not isinstance(error.__cause__, PermissionError) \
|
|
239
|
+
and not isinstance(error, PermissionError):
|
|
203
240
|
raise
|
|
204
241
|
own = own_hf_home()
|
|
205
242
|
own.mkdir(parents=True, exist_ok=True)
|
|
206
243
|
step(f"the usual model folder is not writable; using {own}")
|
|
207
244
|
# huggingface_hub fixed its folders when it was imported: download in a fresh process
|
|
208
245
|
subprocess.run([sys.executable, "-c",
|
|
209
|
-
"from
|
|
210
|
-
|
|
211
|
-
|
|
246
|
+
"from huggingface_hub import hf_hub_download; "
|
|
247
|
+
+ "; ".join(f"hf_hub_download({LOCAL_MODEL!r}, {name!r})"
|
|
248
|
+
for name in MODEL_FILES)],
|
|
212
249
|
env={**os.environ, "HF_HOME": str(own),
|
|
213
250
|
"HF_HUB_DISABLE_SYMLINKS_WARNING": "1"}, check=True)
|
|
214
251
|
|
|
@@ -230,7 +267,8 @@ def main(argv=None):
|
|
|
230
267
|
step("installing packages (the first time takes a few minutes)")
|
|
231
268
|
packages()
|
|
232
269
|
home = Path.home()
|
|
233
|
-
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]
|
|
234
272
|
step("memory connected to: " + (", ".join(apps) if apps else
|
|
235
273
|
"no AI app found yet; install one and run this again"))
|
|
236
274
|
step(capture_service(start=not args.no_start))
|
|
@@ -258,17 +296,23 @@ def uninstall(argv=None):
|
|
|
258
296
|
claude = shutil.which("claude")
|
|
259
297
|
if claude:
|
|
260
298
|
subprocess.run([claude, "mcp", "remove", "-s", "user", NAME], capture_output=True)
|
|
261
|
-
|
|
262
|
-
|
|
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:
|
|
263
304
|
if not config.exists():
|
|
264
305
|
continue
|
|
265
|
-
|
|
266
|
-
|
|
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:
|
|
267
311
|
temporary = config.with_suffix(".graph-mind.tmp")
|
|
268
312
|
temporary.write_text(json.dumps(settings, ensure_ascii=False, indent=2),
|
|
269
313
|
encoding="utf-8")
|
|
270
314
|
os.replace(temporary, config)
|
|
271
|
-
removed.append(
|
|
315
|
+
removed.append(label)
|
|
272
316
|
codex_config = home / ".codex" / "config.toml"
|
|
273
317
|
if codex_config.exists():
|
|
274
318
|
text = codex_config.read_text(encoding="utf-8")
|
|
@@ -36,17 +36,34 @@ def use_own_hf_home() -> None:
|
|
|
36
36
|
use_own_hf_home()
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
MODEL_FILES = ("tokenizer.json", "onnx/model.onnx")
|
|
40
|
+
|
|
41
|
+
|
|
39
42
|
def model_on_disk() -> bool:
|
|
43
|
+
"""Both files the embedder reads are already downloaded (an install from before 0.4 has the
|
|
44
|
+
torch weights in the same folder, but not the ONNX file)."""
|
|
40
45
|
hf_home = Path(os.environ.get("HF_HOME") or Path.home() / ".cache" / "huggingface")
|
|
41
|
-
|
|
46
|
+
snapshots = hf_home / "hub" / ("models--" + LOCAL_MODEL.replace("/", "--")) / "snapshots"
|
|
47
|
+
return any(all((snapshot / name).exists() for name in MODEL_FILES)
|
|
48
|
+
for snapshot in (snapshots.iterdir() if snapshots.is_dir() else []))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def model_paths(model=LOCAL_MODEL) -> tuple:
|
|
52
|
+
"""Local paths of the tokenizer and the ONNX model, downloaded on first use."""
|
|
53
|
+
from huggingface_hub import hf_hub_download
|
|
54
|
+
return tuple(hf_hub_download(model, name) for name in MODEL_FILES)
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
class LocalEmbedder:
|
|
45
|
-
"""On-device multilingual embeddings
|
|
46
|
-
No text leaves the machine
|
|
58
|
+
"""On-device multilingual embeddings: the model's own ONNX export run by ONNX Runtime,
|
|
59
|
+
mean pooling, L2-normalized. No text leaves the machine.
|
|
60
|
+
|
|
61
|
+
Until 0.4 this ran on torch + transformers: same model, same vectors (on 1,000 texts the
|
|
62
|
+
largest difference was 2.4e-7, cosine 1.000000), but a 2 GB install, the Visual C++ runtime,
|
|
63
|
+
and 48-71 s to load on a OneDrive-synced Windows PC against 6-9 s now."""
|
|
47
64
|
|
|
48
|
-
_model = None
|
|
49
|
-
_tok = None
|
|
65
|
+
_model = None # an onnxruntime.InferenceSession
|
|
66
|
+
_tok = None # a tokenizers.Tokenizer
|
|
50
67
|
_lock = threading.RLock() # model load + cache writes are not thread-safe
|
|
51
68
|
|
|
52
69
|
FLUSH_EVERY = 5000 # vectors between writes: bounds lost work without rewriting per call
|
|
@@ -69,11 +86,17 @@ class LocalEmbedder:
|
|
|
69
86
|
|
|
70
87
|
def _load(self):
|
|
71
88
|
if LocalEmbedder._model is None:
|
|
72
|
-
import
|
|
73
|
-
from
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
import onnxruntime
|
|
90
|
+
from tokenizers import Tokenizer
|
|
91
|
+
tokenizer_file, model_file = model_paths(self.model_name)
|
|
92
|
+
tokenizer = Tokenizer.from_file(tokenizer_file)
|
|
93
|
+
tokenizer.enable_truncation(256) # as transformers did: max_length=256
|
|
94
|
+
tokenizer.enable_padding()
|
|
95
|
+
options = onnxruntime.SessionOptions()
|
|
96
|
+
options.log_severity_level = 3 # warnings about unused initializers: noise
|
|
97
|
+
LocalEmbedder._tok = tokenizer
|
|
98
|
+
LocalEmbedder._model = onnxruntime.InferenceSession(
|
|
99
|
+
model_file, options, providers=["CPUExecutionProvider"])
|
|
77
100
|
LocalEmbedder._loaded.set()
|
|
78
101
|
|
|
79
102
|
def ready(self, timeout) -> bool:
|
|
@@ -97,16 +120,18 @@ class LocalEmbedder:
|
|
|
97
120
|
return hashlib.sha256((self.model_name + "\0" + text).encode("utf-8")).hexdigest()
|
|
98
121
|
|
|
99
122
|
def _encode(self, texts):
|
|
100
|
-
import
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
import numpy as np
|
|
124
|
+
encoded = LocalEmbedder._tok.encode_batch(texts)
|
|
125
|
+
ids = np.array([e.ids for e in encoded], dtype=np.int64)
|
|
126
|
+
mask = np.array([e.attention_mask for e in encoded], dtype=np.int64)
|
|
127
|
+
feed = {"input_ids": ids, "attention_mask": mask}
|
|
128
|
+
if "token_type_ids" in {i.name for i in LocalEmbedder._model.get_inputs()}:
|
|
129
|
+
feed["token_type_ids"] = np.zeros_like(ids)
|
|
130
|
+
hidden = LocalEmbedder._model.run(None, feed)[0]
|
|
131
|
+
weights = mask[..., None].astype(np.float32)
|
|
132
|
+
pooled = (hidden * weights).sum(1) / np.clip(weights.sum(1), 1e-9, None)
|
|
133
|
+
pooled /= np.clip(np.linalg.norm(pooled, axis=1, keepdims=True), 1e-12, None)
|
|
134
|
+
return pooled.tolist()
|
|
110
135
|
|
|
111
136
|
def embed(self, texts):
|
|
112
137
|
texts = [t.strip()[:2000] for t in texts]
|
|
@@ -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"
|
|
@@ -18,15 +18,16 @@ classifiers = [
|
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
"torch>=2.2,<3",
|
|
22
21
|
"mcp>=2,<3",
|
|
23
22
|
"numpy",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
23
|
+
"onnxruntime>=1.17",
|
|
24
|
+
"tokenizers>=0.15",
|
|
25
|
+
"huggingface_hub>=0.20",
|
|
26
26
|
# hosts the shared brain's Postgres; built for Python 3.9-3.12 only. Joining another PC's
|
|
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.3.4 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{graph_mind_memory-0.3.4 → graph_mind_memory-0.5.0}/graph_mind_memory.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{graph_mind_memory-0.3.4 → 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
|