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.
- {graph_mind_memory-0.3.0/graph_mind_memory.egg-info → graph_mind_memory-0.3.2}/PKG-INFO +33 -15
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/README.md +32 -14
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/brain_log.py +54 -15
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/conversation_memory.py +38 -1
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_mcp_server.py +17 -1
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2/graph_mind_memory.egg-info}/PKG-INFO +33 -15
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/entry_points.txt +1 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/install.py +104 -15
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/pyproject.toml +2 -1
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/LICENSE +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/associative_memory.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/automatic_capture.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/clock.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/coding_memory.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/development_memory.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/development_paths.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/entity_timeline.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/forget.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/requires.txt +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/graph_mind_memory.egg-info/top_level.txt +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/local_brain.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/local_embedder.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/semantic_memory.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/semantic_recall.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/setup.cfg +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.3.0 → graph_mind_memory-0.3.2}/universal_personal_memory.py +0 -0
- {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.
|
|
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
|
|
@@ -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,
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
is not
|
|
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
|
|
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
|
|
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
|
|
@@ -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,
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
is not
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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,
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
is not
|
|
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
|
|
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
|
|
@@ -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
|
{graph_mind_memory-0.3.0 → 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.0 → 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
|