graph-mind-memory 0.3.1__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.1/graph_mind_memory.egg-info → graph_mind_memory-0.3.2}/PKG-INFO +24 -14
  2. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/README.md +23 -13
  3. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/brain_log.py +54 -15
  4. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_mcp_server.py +17 -1
  5. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2/graph_mind_memory.egg-info}/PKG-INFO +24 -14
  6. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/entry_points.txt +1 -0
  7. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/install.py +104 -15
  8. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/pyproject.toml +2 -1
  9. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/LICENSE +0 -0
  10. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/associative_memory.py +0 -0
  11. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/automatic_capture.py +0 -0
  12. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/automatic_capture_cli.py +0 -0
  13. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/clock.py +0 -0
  14. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/coding_memory.py +0 -0
  15. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/conversation_memory.py +0 -0
  16. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/development_memory.py +0 -0
  17. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/development_paths.py +0 -0
  18. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/embedding_warmup.py +0 -0
  19. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/entity_timeline.py +0 -0
  20. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/forget.py +0 -0
  21. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
  22. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
  23. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/requires.txt +0 -0
  24. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/top_level.txt +0 -0
  25. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/local_brain.py +0 -0
  26. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/local_embedder.py +0 -0
  27. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/semantic_llm_extractor.py +0 -0
  28. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/semantic_memory.py +0 -0
  29. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/semantic_recall.py +0 -0
  30. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/setup.cfg +0 -0
  31. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/temporal_resolver.py +0 -0
  32. {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/universal_personal_memory.py +0 -0
  33. {graph_mind_memory-0.3.1 → 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.1
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
@@ -176,20 +182,19 @@ tell it **where the key is**, not the key: *"my OpenAI key is in 1Password, unde
176
182
 
177
183
  Only what you actually send is captured: the service reads each app's transcript, which is written
178
184
  after you press Enter, so a paste you delete before sending never reaches it. A plain password
179
- 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:
180
190
 
181
191
  ```bash
182
192
  graph-mind-forget # asks for the phrase without showing it, then confirms
183
193
  ```
184
194
 
185
- Or just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
186
- (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
187
- is not saved.
188
-
189
- It deletes every captured turn and memory containing the phrase from this PC and its indexes,
190
- removes their lines from the shared memory, and tells your other PCs to delete their copies on
191
- their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
192
- (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.
193
198
 
194
199
  ---
195
200
 
@@ -262,7 +267,7 @@ python -m unittest discover -p "test_*.py"
262
267
  - **Multi-session questions are the weakest type at 80%.** These are questions that count or
263
268
  combine facts across many conversations.
264
269
  - **The repository still holds the research-phase experiments** next to the product (see
265
- [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
266
271
  modules.
267
272
 
268
273
  The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
@@ -271,11 +276,16 @@ The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
271
276
 
272
277
  | | files |
273
278
  |---|---|
274
- | **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 |
275
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/` |
276
281
  | **Research phase** | the other modules: earlier extraction pipelines and analyses that REPORT.md cites |
277
282
  | **Tests** | `test_*.py` |
278
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
+
279
289
  ## Contributing
280
290
 
281
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
@@ -150,20 +156,19 @@ tell it **where the key is**, not the key: *"my OpenAI key is in 1Password, unde
150
156
 
151
157
  Only what you actually send is captured: the service reads each app's transcript, which is written
152
158
  after you press Enter, so a paste you delete before sending never reaches it. A plain password
153
- 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:
154
164
 
155
165
  ```bash
156
166
  graph-mind-forget # asks for the phrase without showing it, then confirms
157
167
  ```
158
168
 
159
- Or just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
160
- (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
161
- is not saved.
162
-
163
- It deletes every captured turn and memory containing the phrase from this PC and its indexes,
164
- removes their lines from the shared memory, and tells your other PCs to delete their copies on
165
- their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
166
- (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.
167
172
 
168
173
  ---
169
174
 
@@ -236,7 +241,7 @@ python -m unittest discover -p "test_*.py"
236
241
  - **Multi-session questions are the weakest type at 80%.** These are questions that count or
237
242
  combine facts across many conversations.
238
243
  - **The repository still holds the research-phase experiments** next to the product (see
239
- [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
240
245
  modules.
241
246
 
242
247
  The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
@@ -245,11 +250,16 @@ The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
245
250
 
246
251
  | | files |
247
252
  |---|---|
248
- | **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 |
249
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/` |
250
255
  | **Research phase** | the other modules: earlier extraction pipelines and analyses that REPORT.md cites |
251
256
  | **Tests** | `test_*.py` |
252
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
+
253
263
  ## Contributing
254
264
 
255
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
@@ -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.1
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
@@ -176,20 +182,19 @@ tell it **where the key is**, not the key: *"my OpenAI key is in 1Password, unde
176
182
 
177
183
  Only what you actually send is captured: the service reads each app's transcript, which is written
178
184
  after you press Enter, so a paste you delete before sending never reaches it. A plain password
179
- 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:
180
190
 
181
191
  ```bash
182
192
  graph-mind-forget # asks for the phrase without showing it, then confirms
183
193
  ```
184
194
 
185
- Or just ask your AI: *"delete the password I typed earlier"*. It lists masked candidates
186
- (`wifi password is ****`), deletes only the ones you pick, and the conversation about deleting
187
- is not saved.
188
-
189
- It deletes every captured turn and memory containing the phrase from this PC and its indexes,
190
- removes their lines from the shared memory, and tells your other PCs to delete their copies on
191
- their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
192
- (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.
193
198
 
194
199
  ---
195
200
 
@@ -262,7 +267,7 @@ python -m unittest discover -p "test_*.py"
262
267
  - **Multi-session questions are the weakest type at 80%.** These are questions that count or
263
268
  combine facts across many conversations.
264
269
  - **The repository still holds the research-phase experiments** next to the product (see
265
- [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
266
271
  modules.
267
272
 
268
273
  The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
@@ -271,11 +276,16 @@ The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
271
276
 
272
277
  | | files |
273
278
  |---|---|
274
- | **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 |
275
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/` |
276
281
  | **Research phase** | the other modules: earlier extraction pipelines and analyses that REPORT.md cites |
277
282
  | **Tests** | `test_*.py` |
278
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
+
279
289
  ## Contributing
280
290
 
281
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.1"
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