graph-mind-memory 0.3.0__tar.gz → 0.3.2__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.3.0/graph_mind_memory.egg-info → graph_mind_memory-0.3.2}/PKG-INFO +33 -15
  2. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/README.md +32 -14
  3. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/brain_log.py +54 -15
  4. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/conversation_memory.py +38 -1
  5. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_mcp_server.py +17 -1
  6. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2/graph_mind_memory.egg-info}/PKG-INFO +33 -15
  7. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/entry_points.txt +1 -0
  8. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/install.py +104 -15
  9. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/pyproject.toml +2 -1
  10. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/LICENSE +0 -0
  11. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/associative_memory.py +0 -0
  12. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/automatic_capture.py +0 -0
  13. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/automatic_capture_cli.py +0 -0
  14. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/clock.py +0 -0
  15. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/coding_memory.py +0 -0
  16. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/development_memory.py +0 -0
  17. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/development_paths.py +0 -0
  18. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/embedding_warmup.py +0 -0
  19. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/entity_timeline.py +0 -0
  20. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/forget.py +0 -0
  21. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
  22. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
  23. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/requires.txt +0 -0
  24. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/top_level.txt +0 -0
  25. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/local_brain.py +0 -0
  26. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/local_embedder.py +0 -0
  27. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/semantic_llm_extractor.py +0 -0
  28. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/semantic_memory.py +0 -0
  29. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/semantic_recall.py +0 -0
  30. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/setup.cfg +0 -0
  31. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/temporal_resolver.py +0 -0
  32. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/universal_personal_memory.py +0 -0
  33. {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/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.0
3
+ Version: 0.3.2
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
@@ -48,7 +48,7 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
48
48
  </div>
49
49
 
50
50
  > [!NOTE]
51
- > **Alpha (v0.1).** Used daily on Windows 11, and checked in a fresh-install, two-PC end-to-end run.
51
+ > **Alpha.** Used daily on Windows 11, and checked in a fresh-install, two-PC end-to-end run.
52
52
  > The test suite also passes on Linux and macOS in CI, but nobody has used it day to day there yet.
53
53
 
54
54
  ---
@@ -66,7 +66,9 @@ calls Graph-MIND over MCP and gets back a few thousand tokens of the conversatio
66
66
  stores the turns and embeds them as they arrive.
67
67
  - **One memory across your PCs.** One sentence to the AI on the first PC, one command on the
68
68
  others (see [below](#one-memory-across-pcs)).
69
- - **Nothing leaves your machines.** There is no cloud, no account and no telemetry.
69
+ - **No cloud of its own.** No account, no telemetry; the memory stays on your machines. What
70
+ leaves them is what your AI app already sends: the recalled turns go to your model's
71
+ provider with your question.
70
72
 
71
73
  ---
72
74
 
@@ -152,6 +154,10 @@ Then restart your AI apps. The server is also listed in the
152
154
  > cause. Clone to a short path such as `C:\graph-mind`, or enable long paths. The installer prints
153
155
  > the command for that.
154
156
 
157
+ To remove it: `graph-mind-uninstall` unregisters it from every app, removes the login item and
158
+ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `--purge`. Then
159
+ `pip uninstall graph-mind-memory` removes the program.
160
+
155
161
  ---
156
162
 
157
163
  ## What it captures
@@ -164,24 +170,31 @@ Then restart your AI apps. The server is also listed in the
164
170
  | Claude desktop: chat | what the model saves with `brain_remember` |
165
171
 
166
172
  Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
167
- private keys, and passwords inside URLs.
173
+ private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
174
+ and any long machine-random string even from a service no rule knows (by its randomness: 91% of
175
+ random 20-64 character tokens caught; in 246,750 ordinary chat turns it fired 92 times, nearly
176
+ all on real ids and tokens). Commit ids, hashes and UUIDs are kept, since you recall those on
177
+ purpose.
178
+
179
+ There is no switch to keep keys: a recalled memory goes to your model's provider with the question,
180
+ so a stored key would leave your PC the first time it is useful. To have Graph-MIND remember a key,
181
+ tell it **where the key is**, not the key: *"my OpenAI key is in 1Password, under Dev"*.
168
182
 
169
183
  Only what you actually send is captured: the service reads each app's transcript, which is written
170
184
  after you press Enter, so a paste you delete before sending never reaches it. A plain password
171
- like `hunter2` has no recognizable format and is **not** masked. To remove something:
185
+ like `hunter2` has no recognizable format and is **not** masked. To remove something,
186
+
187
+ just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
188
+ (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
189
+ is not saved. Or, from a terminal:
172
190
 
173
191
  ```bash
174
192
  graph-mind-forget # asks for the phrase without showing it, then confirms
175
193
  ```
176
194
 
177
- Or just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
178
- (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
179
- is not saved.
180
-
181
- It deletes every captured turn and memory containing the phrase from this PC and its indexes,
182
- removes their lines from the shared memory, and tells your other PCs to delete their copies on
183
- their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
184
- (for Claude Code, `~/.claude/projects`) is separate and is not touched.
195
+ Either way, what you pick is deleted from this PC and its search indexes, from the shared memory,
196
+ and from your other PCs on their next sync; only ids are shared for that, never the text. Your AI
197
+ app's own history (for Claude Code, `~/.claude/projects`) is separate and is not touched.
185
198
 
186
199
  ---
187
200
 
@@ -254,7 +267,7 @@ python -m unittest discover -p "test_*.py"
254
267
  - **Multi-session questions are the weakest type at 80%.** These are questions that count or
255
268
  combine facts across many conversations.
256
269
  - **The repository still holds the research-phase experiments** next to the product (see
257
- [Repository layout](#repository-layout)); the installed package carries only the 21 product
270
+ [Repository layout](#repository-layout)); the installed package carries only the 22 product
258
271
  modules.
259
272
 
260
273
  The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
@@ -263,11 +276,16 @@ The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
263
276
 
264
277
  | | files |
265
278
  |---|---|
266
- | **Product** (what `pip install graph-mind-memory` installs) | `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `local_brain.py` / `conversation_memory.py` / `coding_memory.py` / `development_memory.py` (stores), `semantic_recall.py` / `local_embedder.py` / `vector_cache.py` / `embedding_warmup.py` (search), and their helpers |
279
+ | **Product** (what `pip install graph-mind-memory` installs) | `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `forget.py` (deleting), `local_brain.py` / `conversation_memory.py` / `coding_memory.py` / `development_memory.py` (stores), `semantic_recall.py` / `local_embedder.py` / `vector_cache.py` / `embedding_warmup.py` (search), and their helpers |
267
280
  | **Benchmarks** | `product_answer_eval.py`, `official_judge_v073.py`, `rival_mem0.py`, `rival_mempalace.py`, `rival_clean_prereg.py`, and the result files under `runs/` |
268
281
  | **Research phase** | the other modules: earlier extraction pipelines and analyses that REPORT.md cites |
269
282
  | **Tests** | `test_*.py` |
270
283
 
284
+ ## Security
285
+
286
+ Graph-MIND holds your conversations. Report vulnerabilities privately (GitHub **Security → Report
287
+ a vulnerability**); [SECURITY.md](SECURITY.md) lists what is in scope and how the data is handled.
288
+
271
289
  ## Contributing
272
290
 
273
291
  Issues and pull requests are welcome. Contributions are accepted under the [CLA](CLA.md), which
@@ -22,7 +22,7 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
22
22
  </div>
23
23
 
24
24
  > [!NOTE]
25
- > **Alpha (v0.1).** Used daily on Windows 11, and checked in a fresh-install, two-PC end-to-end run.
25
+ > **Alpha.** Used daily on Windows 11, and checked in a fresh-install, two-PC end-to-end run.
26
26
  > The test suite also passes on Linux and macOS in CI, but nobody has used it day to day there yet.
27
27
 
28
28
  ---
@@ -40,7 +40,9 @@ calls Graph-MIND over MCP and gets back a few thousand tokens of the conversatio
40
40
  stores the turns and embeds them as they arrive.
41
41
  - **One memory across your PCs.** One sentence to the AI on the first PC, one command on the
42
42
  others (see [below](#one-memory-across-pcs)).
43
- - **Nothing leaves your machines.** There is no cloud, no account and no telemetry.
43
+ - **No cloud of its own.** No account, no telemetry; the memory stays on your machines. What
44
+ leaves them is what your AI app already sends: the recalled turns go to your model's
45
+ provider with your question.
44
46
 
45
47
  ---
46
48
 
@@ -126,6 +128,10 @@ Then restart your AI apps. The server is also listed in the
126
128
  > cause. Clone to a short path such as `C:\graph-mind`, or enable long paths. The installer prints
127
129
  > the command for that.
128
130
 
131
+ To remove it: `graph-mind-uninstall` unregisters it from every app, removes the login item and
132
+ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `--purge`. Then
133
+ `pip uninstall graph-mind-memory` removes the program.
134
+
129
135
  ---
130
136
 
131
137
  ## What it captures
@@ -138,24 +144,31 @@ Then restart your AI apps. The server is also listed in the
138
144
  | Claude desktop: chat | what the model saves with `brain_remember` |
139
145
 
140
146
  Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
141
- private keys, and passwords inside URLs.
147
+ private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
148
+ and any long machine-random string even from a service no rule knows (by its randomness: 91% of
149
+ random 20-64 character tokens caught; in 246,750 ordinary chat turns it fired 92 times, nearly
150
+ all on real ids and tokens). Commit ids, hashes and UUIDs are kept, since you recall those on
151
+ purpose.
152
+
153
+ There is no switch to keep keys: a recalled memory goes to your model's provider with the question,
154
+ so a stored key would leave your PC the first time it is useful. To have Graph-MIND remember a key,
155
+ tell it **where the key is**, not the key: *"my OpenAI key is in 1Password, under Dev"*.
142
156
 
143
157
  Only what you actually send is captured: the service reads each app's transcript, which is written
144
158
  after you press Enter, so a paste you delete before sending never reaches it. A plain password
145
- like `hunter2` has no recognizable format and is **not** masked. To remove something:
159
+ like `hunter2` has no recognizable format and is **not** masked. To remove something,
160
+
161
+ just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
162
+ (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
163
+ is not saved. Or, from a terminal:
146
164
 
147
165
  ```bash
148
166
  graph-mind-forget # asks for the phrase without showing it, then confirms
149
167
  ```
150
168
 
151
- Or just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
152
- (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
153
- is not saved.
154
-
155
- It deletes every captured turn and memory containing the phrase from this PC and its indexes,
156
- removes their lines from the shared memory, and tells your other PCs to delete their copies on
157
- their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
158
- (for Claude Code, `~/.claude/projects`) is separate and is not touched.
169
+ Either way, what you pick is deleted from this PC and its search indexes, from the shared memory,
170
+ and from your other PCs on their next sync; only ids are shared for that, never the text. Your AI
171
+ app's own history (for Claude Code, `~/.claude/projects`) is separate and is not touched.
159
172
 
160
173
  ---
161
174
 
@@ -228,7 +241,7 @@ python -m unittest discover -p "test_*.py"
228
241
  - **Multi-session questions are the weakest type at 80%.** These are questions that count or
229
242
  combine facts across many conversations.
230
243
  - **The repository still holds the research-phase experiments** next to the product (see
231
- [Repository layout](#repository-layout)); the installed package carries only the 21 product
244
+ [Repository layout](#repository-layout)); the installed package carries only the 22 product
232
245
  modules.
233
246
 
234
247
  The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
@@ -237,11 +250,16 @@ The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
237
250
 
238
251
  | | files |
239
252
  |---|---|
240
- | **Product** (what `pip install graph-mind-memory` installs) | `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `local_brain.py` / `conversation_memory.py` / `coding_memory.py` / `development_memory.py` (stores), `semantic_recall.py` / `local_embedder.py` / `vector_cache.py` / `embedding_warmup.py` (search), and their helpers |
253
+ | **Product** (what `pip install graph-mind-memory` installs) | `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `forget.py` (deleting), `local_brain.py` / `conversation_memory.py` / `coding_memory.py` / `development_memory.py` (stores), `semantic_recall.py` / `local_embedder.py` / `vector_cache.py` / `embedding_warmup.py` (search), and their helpers |
241
254
  | **Benchmarks** | `product_answer_eval.py`, `official_judge_v073.py`, `rival_mem0.py`, `rival_mempalace.py`, `rival_clean_prereg.py`, and the result files under `runs/` |
242
255
  | **Research phase** | the other modules: earlier extraction pipelines and analyses that REPORT.md cites |
243
256
  | **Tests** | `test_*.py` |
244
257
 
258
+ ## Security
259
+
260
+ Graph-MIND holds your conversations. Report vulnerabilities privately (GitHub **Security → Report
261
+ a vulnerability**); [SECURITY.md](SECURITY.md) lists what is in scope and how the data is handled.
262
+
245
263
  ## Contributing
246
264
 
247
265
  Issues and pull requests are welcome. Contributions are accepted under the [CLA](CLA.md), which
@@ -86,35 +86,47 @@ def local_postgres() -> str:
86
86
  its brain needs a port that stays put and a password for anyone not on this PC. The memory
87
87
  lives in database "graphmind" under role "graphmind", which is not a superuser: that is the
88
88
  role other PCs log in as."""
89
- if "uri" not in _LOCAL_URI:
89
+ try:
90
+ from pgserver._commands import initdb, pg_ctl
91
+ except ImportError:
92
+ raise RuntimeError(
93
+ "Hosting the shared brain on this PC needs pgserver, which is built for Python "
94
+ "3.9-3.12 only. Run Graph-MIND on this PC with Python 3.12, or use a synced "
95
+ "folder instead. Joining a brain another PC hosts works on any Python.") from None
96
+ from pgserver.utils import PostmasterInfo
97
+ data = _pg_data()
98
+ running = PostmasterInfo.read_from_pgdata(data) if (data / "PG_VERSION").exists() else None
99
+ # Checked on every call, not once per process: a server that stopped (reboot, a crash, a
100
+ # console closing on it) is started again by whoever needs it next, instead of every
101
+ # client failing until someone notices.
102
+ if running is None or not running.is_running():
90
103
  import fasteners
91
- try:
92
- from pgserver._commands import initdb, pg_ctl
93
- except ImportError:
94
- raise RuntimeError(
95
- "Hosting the shared brain on this PC needs pgserver, which is built for Python "
96
- "3.9-3.12 only. Run Graph-MIND on this PC with Python 3.12, or use a synced "
97
- "folder instead. Joining a brain another PC hosts works on any Python.") from None
98
- from pgserver.utils import PostmasterInfo
99
- data = _pg_data()
100
104
  data.parent.mkdir(parents=True, exist_ok=True)
101
105
  with fasteners.InterProcessLock(str(data.parent / "postgres.lock")):
102
106
  fresh = not (data / "PG_VERSION").exists()
103
107
  if fresh:
104
- initdb(["--auth=trust", "--encoding=utf8", "-U", "postgres"], pgdata=data)
108
+ initdb(["--auth=trust", "--encoding=utf8", "-U", "postgres"], pgdata=data,
109
+ **_DETACHED)
105
110
  running = PostmasterInfo.read_from_pgdata(data)
106
111
  if running is None or not running.is_running():
107
112
  pg_ctl(["-w", "-o", f"-p {PORT}", "-l", str(data / "log"), "start"],
108
- pgdata=data, timeout=60)
113
+ pgdata=data, timeout=60, **_DETACHED)
109
114
  if fresh:
110
115
  import psycopg
111
116
  with psycopg.connect(_admin_uri(), autocommit=True) as admin:
112
117
  admin.execute("CREATE ROLE graphmind LOGIN")
113
118
  admin.execute("CREATE DATABASE graphmind OWNER graphmind")
114
- _LOCAL_URI["uri"] = f"postgresql://graphmind@127.0.0.1:{PORT}/graphmind"
119
+ _LOCAL_URI["uri"] = f"postgresql://graphmind@127.0.0.1:{PORT}/graphmind"
115
120
  return _LOCAL_URI["uri"]
116
121
 
117
122
 
123
+ # Started from Claude's or Codex's process, the server shared that program's console, and closing
124
+ # it sent Ctrl+C to Postgres too (0xC000013A in its log): the hub went down with an app window.
125
+ # Its own process group, with no console, it outlives whoever happened to start it.
126
+ _DETACHED = ({"creationflags": 0x00000008 | 0x00000200} # DETACHED_PROCESS | NEW_PROCESS_GROUP
127
+ if os.name == "nt" else {"start_new_session": True})
128
+
129
+
118
130
  def _admin_uri() -> str:
119
131
  return f"postgresql://postgres@127.0.0.1:{PORT}/postgres"
120
132
 
@@ -145,7 +157,8 @@ def share(open_firewall: bool = True) -> dict:
145
157
  if rule not in hba.read_text(encoding="utf-8"):
146
158
  with hba.open("a", encoding="utf-8") as stream:
147
159
  stream.write("\n# Graph-MIND: other PCs, with the password\n" + rule + "\n")
148
- pg_ctl(["-w", "-m", "fast", "restart"] if restart else ["reload"], pgdata=data, timeout=60)
160
+ pg_ctl(["-w", "-m", "fast", "restart"] if restart else ["reload"], pgdata=data, timeout=60,
161
+ **_DETACHED)
149
162
  set_folder("local")
150
163
  _save({"share_password": password})
151
164
  hosts = _addresses()
@@ -281,7 +294,7 @@ def append_many(shared: Path, items) -> int:
281
294
  if not lines:
282
295
  return 0
283
296
  if is_database(shared):
284
- return _pg_append(shared, lines)
297
+ return _pg_append_or_queue(shared, lines)
285
298
  log = Path(shared).expanduser() / "log" / f"{device_name()}.jsonl"
286
299
  log.parent.mkdir(parents=True, exist_ok=True)
287
300
  with log.open("a", encoding="utf-8") as stream:
@@ -357,6 +370,32 @@ def _pg(url):
357
370
  return connection
358
371
 
359
372
 
373
+ def _outbox() -> Path:
374
+ from development_paths import graph_mind_home
375
+ return graph_mind_home() / "outbox.jsonl"
376
+
377
+
378
+ def _pg_append_or_queue(url, lines) -> int:
379
+ """Append to the hub; when it cannot be reached, keep the lines in a local outbox and send
380
+ them with the next append that gets through. The hub was down for four days once and every
381
+ turn of those days stayed on the PC that captured it until it was sent by hand."""
382
+ import fasteners
383
+ outbox = _outbox()
384
+ outbox.parent.mkdir(parents=True, exist_ok=True)
385
+ with fasteners.InterProcessLock(str(outbox.with_suffix(".lock"))):
386
+ waiting = ([l for l in outbox.read_text(encoding="utf-8").splitlines() if l.strip()]
387
+ if outbox.exists() else [])
388
+ try:
389
+ _pg_append(url, waiting + lines)
390
+ except Exception:
391
+ with outbox.open("a", encoding="utf-8") as stream:
392
+ stream.write("\n".join(lines) + "\n")
393
+ return len(lines) # queued, not lost
394
+ if waiting:
395
+ outbox.unlink()
396
+ return len(lines)
397
+
398
+
360
399
  def _pg_append(url, lines) -> int:
361
400
  with _pg(url) as connection:
362
401
  # one writer at a time: a row can then never commit after a later sequence number has
@@ -5,8 +5,10 @@ from development_paths import use_wal
5
5
 
6
6
  from datetime import datetime
7
7
  from pathlib import Path
8
+ import collections
8
9
  import hashlib
9
10
  import json
11
+ import math
10
12
  import os
11
13
  import re
12
14
  import sqlite3
@@ -30,7 +32,34 @@ SECRET_PATTERNS = (
30
32
  re.DOTALL), "[REDACTED_PRIVATE_KEY]"),
31
33
  # a database URL carries its password: postgres://user:PASSWORD@host
32
34
  (re.compile(r"(\b[a-z][a-z0-9+.-]*://[^\s:/@]+:)[^\s@/]+(@)"), r"\1[REDACTED]\2"),
35
+ # a password written after its name: "password: x", "pwd=x", "비밀번호: x", "비번=x"
36
+ (re.compile(r"(?i)((?<![a-z])(?:password|passwd|pwd)\s*[:=]\s*)\S+"), r"\1[REDACTED]"),
37
+ (re.compile(r"((?:비밀번호|비번|암호)\s*[:=]\s*)\S+"), r"\1[REDACTED]"),
33
38
  )
39
+ # A token no pattern above knows: long, machine-random (mixed case and digits, no word-shaped
40
+ # pieces, high character entropy). Measured: catches 91% of random 20-64 character tokens; in
41
+ # LongMemEval's 246,750 chat turns it fires 92 times, nearly all on real ids and tokens (an
42
+ # OAuth token, Drive file ids, ad click ids); in this repository's code and docs, never.
43
+ # Commit ids, hashes and UUIDs are left alone: those get recalled on purpose.
44
+ RANDOM_CANDIDATE = re.compile(r"(?<![A-Za-z0-9_\-])[A-Za-z0-9_\-]{20,}(?![A-Za-z0-9_\-])")
45
+ UUID = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
46
+ WORD_PIECE = re.compile(r"[A-Z]?[a-z]+|[A-Z]+(?![a-z])")
47
+
48
+
49
+ def _looks_random(token):
50
+ if UUID.fullmatch(token):
51
+ return False
52
+ core = re.sub(r"[_\-]", "", token)
53
+ if len(core) < 18 or re.fullmatch(r"[0-9a-fA-F]+", core):
54
+ return False
55
+ if not (any(c.isupper() for c in core) and any(c.islower() for c in core)
56
+ and sum(c.isdigit() for c in core) >= 2):
57
+ return False
58
+ pieces = WORD_PIECE.findall(core) # code names are words strung together
59
+ if pieces and sum(map(len, pieces)) / len(pieces) >= 3.0:
60
+ return False
61
+ counts = collections.Counter(core)
62
+ return -sum(n / len(core) * math.log2(n / len(core)) for n in counts.values()) >= 3.7
34
63
  AUTOMATIC_BLOCKS = re.compile(
35
64
  r"<(?:environment_context|recommended_plugins|in-app-browser-context)\b[^>]*>.*?"
36
65
  r"</(?:environment_context|recommended_plugins|in-app-browser-context)>",
@@ -56,7 +85,15 @@ def _redact(value):
56
85
  for pattern, replacement in SECRET_PATTERNS:
57
86
  cleaned, replacements = pattern.subn(replacement, cleaned)
58
87
  count += replacements
59
- return cleaned.strip(), count
88
+ found = [0]
89
+
90
+ def unknown_token(match):
91
+ if not _looks_random(match.group(0)):
92
+ return match.group(0)
93
+ found[0] += 1
94
+ return "[REDACTED_SECRET]"
95
+ cleaned = RANDOM_CANDIDATE.sub(unknown_token, cleaned)
96
+ return cleaned.strip(), count + found[0]
60
97
 
61
98
 
62
99
  def _message_text(payload):
@@ -24,7 +24,23 @@ if hasattr(sys.stderr, "reconfigure"):
24
24
  sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace")
25
25
 
26
26
 
27
- SERVER_VERSION = "0.5.0"
27
+ def _version() -> str:
28
+ """The package version: pyproject.toml beside this file in a source checkout, else the
29
+ installed distribution's. A hand-kept constant here had drifted to 0.5.0 at release 0.3.1."""
30
+ import re
31
+ project = Path(__file__).with_name("pyproject.toml")
32
+ if project.exists():
33
+ found = re.search(r'^version = "([^"]+)"', project.read_text(encoding="utf-8"), re.M)
34
+ if found:
35
+ return found.group(1)
36
+ try:
37
+ from importlib.metadata import version
38
+ return version("graph-mind-memory")
39
+ except Exception:
40
+ return "0+unknown"
41
+
42
+
43
+ SERVER_VERSION = _version()
28
44
  # The embedding model is loaded from the local Hugging Face cache, but the library still asks the
29
45
  # hub over the network whether a newer revision exists, every time it loads. That made the first
30
46
  # save of a session take ~95 s and sent requests off the machine for no reason. Once the model is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graph-mind-memory
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -48,7 +48,7 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
48
48
  </div>
49
49
 
50
50
  > [!NOTE]
51
- > **Alpha (v0.1).** Used daily on Windows 11, and checked in a fresh-install, two-PC end-to-end run.
51
+ > **Alpha.** Used daily on Windows 11, and checked in a fresh-install, two-PC end-to-end run.
52
52
  > The test suite also passes on Linux and macOS in CI, but nobody has used it day to day there yet.
53
53
 
54
54
  ---
@@ -66,7 +66,9 @@ calls Graph-MIND over MCP and gets back a few thousand tokens of the conversatio
66
66
  stores the turns and embeds them as they arrive.
67
67
  - **One memory across your PCs.** One sentence to the AI on the first PC, one command on the
68
68
  others (see [below](#one-memory-across-pcs)).
69
- - **Nothing leaves your machines.** There is no cloud, no account and no telemetry.
69
+ - **No cloud of its own.** No account, no telemetry; the memory stays on your machines. What
70
+ leaves them is what your AI app already sends: the recalled turns go to your model's
71
+ provider with your question.
70
72
 
71
73
  ---
72
74
 
@@ -152,6 +154,10 @@ Then restart your AI apps. The server is also listed in the
152
154
  > cause. Clone to a short path such as `C:\graph-mind`, or enable long paths. The installer prints
153
155
  > the command for that.
154
156
 
157
+ To remove it: `graph-mind-uninstall` unregisters it from every app, removes the login item and
158
+ stops the capture service. Your memory stays in `~/.graph-mind` unless you add `--purge`. Then
159
+ `pip uninstall graph-mind-memory` removes the program.
160
+
155
161
  ---
156
162
 
157
163
  ## What it captures
@@ -164,24 +170,31 @@ Then restart your AI apps. The server is also listed in the
164
170
  | Claude desktop: chat | what the model saves with `brain_remember` |
165
171
 
166
172
  Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
167
- private keys, and passwords inside URLs.
173
+ private keys, passwords inside URLs, values written after `password:` / `api_key=` / `token:`,
174
+ and any long machine-random string even from a service no rule knows (by its randomness: 91% of
175
+ random 20-64 character tokens caught; in 246,750 ordinary chat turns it fired 92 times, nearly
176
+ all on real ids and tokens). Commit ids, hashes and UUIDs are kept, since you recall those on
177
+ purpose.
178
+
179
+ There is no switch to keep keys: a recalled memory goes to your model's provider with the question,
180
+ so a stored key would leave your PC the first time it is useful. To have Graph-MIND remember a key,
181
+ tell it **where the key is**, not the key: *"my OpenAI key is in 1Password, under Dev"*.
168
182
 
169
183
  Only what you actually send is captured: the service reads each app's transcript, which is written
170
184
  after you press Enter, so a paste you delete before sending never reaches it. A plain password
171
- like `hunter2` has no recognizable format and is **not** masked. To remove something:
185
+ like `hunter2` has no recognizable format and is **not** masked. To remove something,
186
+
187
+ just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
188
+ (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
189
+ is not saved. Or, from a terminal:
172
190
 
173
191
  ```bash
174
192
  graph-mind-forget # asks for the phrase without showing it, then confirms
175
193
  ```
176
194
 
177
- Or just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
178
- (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
179
- is not saved.
180
-
181
- It deletes every captured turn and memory containing the phrase from this PC and its indexes,
182
- removes their lines from the shared memory, and tells your other PCs to delete their copies on
183
- their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
184
- (for Claude Code, `~/.claude/projects`) is separate and is not touched.
195
+ Either way, what you pick is deleted from this PC and its search indexes, from the shared memory,
196
+ and from your other PCs on their next sync; only ids are shared for that, never the text. Your AI
197
+ app's own history (for Claude Code, `~/.claude/projects`) is separate and is not touched.
185
198
 
186
199
  ---
187
200
 
@@ -254,7 +267,7 @@ python -m unittest discover -p "test_*.py"
254
267
  - **Multi-session questions are the weakest type at 80%.** These are questions that count or
255
268
  combine facts across many conversations.
256
269
  - **The repository still holds the research-phase experiments** next to the product (see
257
- [Repository layout](#repository-layout)); the installed package carries only the 21 product
270
+ [Repository layout](#repository-layout)); the installed package carries only the 22 product
258
271
  modules.
259
272
 
260
273
  The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
@@ -263,11 +276,16 @@ The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
263
276
 
264
277
  | | files |
265
278
  |---|---|
266
- | **Product** (what `pip install graph-mind-memory` installs) | `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `local_brain.py` / `conversation_memory.py` / `coding_memory.py` / `development_memory.py` (stores), `semantic_recall.py` / `local_embedder.py` / `vector_cache.py` / `embedding_warmup.py` (search), and their helpers |
279
+ | **Product** (what `pip install graph-mind-memory` installs) | `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `forget.py` (deleting), `local_brain.py` / `conversation_memory.py` / `coding_memory.py` / `development_memory.py` (stores), `semantic_recall.py` / `local_embedder.py` / `vector_cache.py` / `embedding_warmup.py` (search), and their helpers |
267
280
  | **Benchmarks** | `product_answer_eval.py`, `official_judge_v073.py`, `rival_mem0.py`, `rival_mempalace.py`, `rival_clean_prereg.py`, and the result files under `runs/` |
268
281
  | **Research phase** | the other modules: earlier extraction pipelines and analyses that REPORT.md cites |
269
282
  | **Tests** | `test_*.py` |
270
283
 
284
+ ## Security
285
+
286
+ Graph-MIND holds your conversations. Report vulnerabilities privately (GitHub **Security → Report
287
+ a vulnerability**); [SECURITY.md](SECURITY.md) lists what is in scope and how the data is handled.
288
+
271
289
  ## Contributing
272
290
 
273
291
  Issues and pull requests are welcome. Contributions are accepted under the [CLA](CLA.md), which
@@ -3,3 +3,4 @@ graph-mind = graph_mind_mcp_server:main
3
3
  graph-mind-capture = automatic_capture_cli:main
4
4
  graph-mind-forget = forget:main
5
5
  graph-mind-install = install:main
6
+ graph-mind-uninstall = install:uninstall
@@ -67,34 +67,57 @@ def claude_code(home: Path) -> str:
67
67
  return ""
68
68
 
69
69
 
70
- def claude_desktop(home: Path) -> list[str]:
70
+ def _desktop_roots(home: Path) -> list[Path]:
71
71
  roots = [Path(os.environ.get("APPDATA", home / "AppData" / "Roaming")) / "Claude",
72
72
  home / "Library" / "Application Support" / "Claude"]
73
73
  packages_dir = home / "AppData" / "Local" / "Packages"
74
74
  if packages_dir.is_dir(): # the Microsoft Store build keeps its own copy
75
75
  roots += [p / "LocalCache" / "Roaming" / "Claude" for p in packages_dir.glob("Claude_*")]
76
+ return roots
77
+
78
+
79
+ def claude_desktop(home: Path) -> list[str]:
76
80
  done = []
77
- for root in roots:
81
+ for root in _desktop_roots(home):
78
82
  if root.is_dir():
79
83
  _json_entry(root / "claude_desktop_config.json")
80
84
  done.append("Claude Desktop")
81
85
  return done[:1]
82
86
 
83
87
 
88
+ def _without_codex_block(text: str) -> str:
89
+ """The Codex config without Graph-MIND's server table (and its env table); the rest kept."""
90
+ header = f"[mcp_servers.{NAME}]"
91
+ if header not in text:
92
+ return text
93
+ kept, skipping = [], False
94
+ for line in text.splitlines():
95
+ if line.strip().startswith("["):
96
+ skipping = line.strip() in (header, f"[mcp_servers.{NAME}.env]")
97
+ if not skipping:
98
+ kept.append(line)
99
+ return "\n".join(kept).rstrip() + "\n"
100
+
101
+
102
+ def _login_item() -> Path:
103
+ """Where the capture service is registered to start at login, on this OS."""
104
+ system = platform.system()
105
+ if system == "Windows":
106
+ return (Path(os.environ["APPDATA"]) / "Microsoft" / "Windows" / "Start Menu" / "Programs"
107
+ / "Startup" / "Graph-MIND Automatic Capture.lnk")
108
+ if system == "Darwin":
109
+ return Path.home() / "Library" / "LaunchAgents" / "com.graph-mind.capture.plist"
110
+ config = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config")
111
+ return config / "autostart" / "graph-mind-capture.desktop"
112
+
113
+
84
114
  def codex(home: Path) -> str:
85
115
  config = home / ".codex" / "config.toml"
86
116
  if not config.parent.is_dir():
87
117
  return ""
88
118
  text = config.read_text(encoding="utf-8") if config.exists() else ""
89
119
  header = f"[mcp_servers.{NAME}]"
90
- if header in text: # drop the old block (and its env table), keep the rest
91
- kept, skipping = [], False
92
- for line in text.splitlines():
93
- if line.strip().startswith("["):
94
- skipping = line.strip() in (header, f"[mcp_servers.{NAME}.env]")
95
- if not skipping:
96
- kept.append(line)
97
- text = "\n".join(kept).rstrip() + "\n"
120
+ text = _without_codex_block(text)
98
121
  environment = "\n".join(f"{key} = '{value}'" for key, value in ENV.items())
99
122
  # required: otherwise Codex leaves out a server still starting when the first question comes
100
123
  text += (f"\n{header}\nrequired = true\nstartup_timeout_sec = 60\n"
@@ -119,8 +142,7 @@ def capture_service(start: bool) -> str:
119
142
  system = platform.system()
120
143
  if system == "Windows":
121
144
  python = Path(sys.executable).with_name("pythonw.exe") # no console window
122
- startup = (Path(os.environ["APPDATA"]) / "Microsoft" / "Windows" / "Start Menu"
123
- / "Programs" / "Startup" / "Graph-MIND Automatic Capture.lnk")
145
+ startup = _login_item()
124
146
  startup.parent.mkdir(parents=True, exist_ok=True)
125
147
  shortcut = (f"$s=(New-Object -ComObject WScript.Shell).CreateShortcut('{startup}');"
126
148
  f"$s.TargetPath='{python}';$s.Arguments='\"{script}\" run';"
@@ -130,15 +152,14 @@ def capture_service(start: bool) -> str:
130
152
  elif system == "Darwin":
131
153
  import plistlib
132
154
  python = Path(sys.executable)
133
- agent = Path.home() / "Library" / "LaunchAgents" / "com.graph-mind.capture.plist"
155
+ agent = _login_item()
134
156
  agent.parent.mkdir(parents=True, exist_ok=True)
135
157
  agent.write_bytes(plistlib.dumps({
136
158
  "Label": "com.graph-mind.capture", "RunAtLoad": True, "WorkingDirectory": str(HERE),
137
159
  "ProgramArguments": [str(python), str(script), "run"]}))
138
160
  else: # Linux desktops: XDG autostart
139
161
  python = Path(sys.executable)
140
- config = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config")
141
- entry = config / "autostart" / "graph-mind-capture.desktop"
162
+ entry = _login_item()
142
163
  entry.parent.mkdir(parents=True, exist_ok=True)
143
164
  entry.write_text("[Desktop Entry]\nType=Application\nName=Graph-MIND capture\n"
144
165
  f"Exec=\"{python}\" \"{script}\" run\nPath={HERE}\n"
@@ -224,5 +245,73 @@ def main(argv=None):
224
245
  print("\nDone. Restart your AI apps so they load the memory.")
225
246
 
226
247
 
248
+ def uninstall(argv=None):
249
+ """Undo install: unregister from every AI app, remove the login item, stop the capture
250
+ service. The memory itself stays unless --purge, so reinstalling picks up where it was."""
251
+ parser = argparse.ArgumentParser(description="Remove Graph-MIND from this PC's AI apps.")
252
+ parser.add_argument("--purge", action="store_true",
253
+ help="also delete this PC's memory (~/.graph-mind); cannot be undone")
254
+ parser.add_argument("--yes", action="store_true", help="do not ask before --purge")
255
+ args = parser.parse_args(argv)
256
+ sys.path.insert(0, str(HERE))
257
+ home, removed = Path.home(), []
258
+ claude = shutil.which("claude")
259
+ if claude:
260
+ subprocess.run([claude, "mcp", "remove", "-s", "user", NAME], capture_output=True)
261
+ for config in [home / ".claude.json"] + [r / "claude_desktop_config.json"
262
+ for r in _desktop_roots(home)]:
263
+ if not config.exists():
264
+ continue
265
+ settings = json.loads(config.read_text(encoding="utf-8"))
266
+ if settings.get("mcpServers", {}).pop(NAME, None) is not None:
267
+ temporary = config.with_suffix(".graph-mind.tmp")
268
+ temporary.write_text(json.dumps(settings, ensure_ascii=False, indent=2),
269
+ encoding="utf-8")
270
+ os.replace(temporary, config)
271
+ removed.append("Claude Code" if config.name == ".claude.json" else "Claude Desktop")
272
+ codex_config = home / ".codex" / "config.toml"
273
+ if codex_config.exists():
274
+ text = codex_config.read_text(encoding="utf-8")
275
+ if text != _without_codex_block(text):
276
+ codex_config.write_text(_without_codex_block(text), encoding="utf-8")
277
+ removed.append("Codex")
278
+ step("memory removed from: " + (", ".join(dict.fromkeys(removed)) or "no app had it"))
279
+ item = _login_item()
280
+ if item.exists():
281
+ if platform.system() == "Darwin":
282
+ subprocess.run(["launchctl", "unload", str(item)], capture_output=True)
283
+ item.unlink()
284
+ step("capture service no longer starts at login")
285
+ from automatic_capture import default_service_state_dir
286
+ try:
287
+ import psutil
288
+ service = psutil.Process(int((default_service_state_dir() / "service.pid")
289
+ .read_text(encoding="ascii")))
290
+ if "automatic_capture_cli" in " ".join(service.cmdline()):
291
+ service.terminate()
292
+ step("capture service stopped")
293
+ except Exception: # no pid file, not running, or already gone
294
+ pass
295
+ from development_paths import graph_mind_home
296
+ data = graph_mind_home()
297
+ if args.purge and data.exists():
298
+ if not args.yes and input(f"Delete {data} and everything this PC remembers? "
299
+ "This cannot be undone. [y/N] ").lower() != "y":
300
+ print("Memory kept.")
301
+ else:
302
+ if (data / "postgres" / "postmaster.pid").exists(): # this PC hosts the hub
303
+ try:
304
+ from pgserver._commands import pg_ctl
305
+ pg_ctl(["-w", "-m", "fast", "stop"], pgdata=data / "postgres")
306
+ except Exception:
307
+ pass
308
+ shutil.rmtree(data, ignore_errors=True)
309
+ step(f"deleted {data}")
310
+ elif data.exists():
311
+ step(f"your memory is kept in {data} (graph-mind-uninstall --purge deletes it)")
312
+ print("\nDone. Restart your AI apps. To remove the program too: "
313
+ "pip uninstall graph-mind-memory")
314
+
315
+
227
316
  if __name__ == "__main__":
228
317
  main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graph-mind-memory"
7
- version = "0.3.0"
7
+ version = "0.3.2"
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"
@@ -38,6 +38,7 @@ Issues = "https://github.com/goyohan0611-png/graph-mind/issues"
38
38
  [project.scripts]
39
39
  graph-mind = "graph_mind_mcp_server:main"
40
40
  graph-mind-install = "install:main"
41
+ graph-mind-uninstall = "install:uninstall"
41
42
  graph-mind-capture = "automatic_capture_cli:main"
42
43
  graph-mind-forget = "forget:main"
43
44