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.
- {graph_mind_memory-0.3.1/graph_mind_memory.egg-info → graph_mind_memory-0.3.2}/PKG-INFO +24 -14
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/README.md +23 -13
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/brain_log.py +54 -15
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_mcp_server.py +17 -1
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2/graph_mind_memory.egg-info}/PKG-INFO +24 -14
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/entry_points.txt +1 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/install.py +104 -15
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/pyproject.toml +2 -1
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/LICENSE +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/associative_memory.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/automatic_capture.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/clock.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/coding_memory.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/conversation_memory.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/development_memory.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/development_paths.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/entity_timeline.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/forget.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/requires.txt +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/top_level.txt +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/local_brain.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/local_embedder.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/semantic_memory.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/semantic_recall.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/setup.cfg +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/universal_personal_memory.py +0 -0
- {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.
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
is not
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
is not
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
is not
|
|
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
|
|
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
|
|
@@ -67,34 +67,57 @@ def claude_code(home: Path) -> str:
|
|
|
67
67
|
return ""
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
|
|
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.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{graph_mind_memory-0.3.1 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|