graph-mind-memory 0.2.0__tar.gz → 0.2.1__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.2.0/graph_mind_memory.egg-info → graph_mind_memory-0.2.1}/PKG-INFO +14 -1
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/README.md +13 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/brain_log.py +7 -1
- graph_mind_memory-0.2.1/forget.py +166 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1/graph_mind_memory.egg-info}/PKG-INFO +14 -1
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/graph_mind_memory.egg-info/SOURCES.txt +1 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/graph_mind_memory.egg-info/entry_points.txt +1 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/graph_mind_memory.egg-info/top_level.txt +1 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/pyproject.toml +3 -1
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/LICENSE +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/associative_memory.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/automatic_capture.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/clock.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/coding_memory.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/conversation_memory.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/development_memory.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/development_paths.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/entity_timeline.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/graph_mind_mcp_server.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/graph_mind_memory.egg-info/requires.txt +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/install.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/local_brain.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/local_embedder.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/semantic_memory.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/semantic_recall.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/setup.cfg +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/universal_personal_memory.py +0 -0
- {graph_mind_memory-0.2.0 → graph_mind_memory-0.2.1}/vector_cache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-mind-memory
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -166,6 +166,19 @@ Then restart your AI apps. The server is also listed in the
|
|
|
166
166
|
Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
|
|
167
167
|
private keys, and passwords inside URLs.
|
|
168
168
|
|
|
169
|
+
Only what you actually send is captured: the service reads each app's transcript, which is written
|
|
170
|
+
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:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
graph-mind-forget # asks for the phrase without showing it, then confirms
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It deletes every captured turn and memory containing the phrase from this PC and its indexes,
|
|
178
|
+
removes their lines from the shared memory, and tells your other PCs to delete their copies on
|
|
179
|
+
their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
|
|
180
|
+
(for Claude Code, `~/.claude/projects`) is separate and is not touched.
|
|
181
|
+
|
|
169
182
|
---
|
|
170
183
|
|
|
171
184
|
## MCP tools
|
|
@@ -140,6 +140,19 @@ Then restart your AI apps. The server is also listed in the
|
|
|
140
140
|
Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
|
|
141
141
|
private keys, and passwords inside URLs.
|
|
142
142
|
|
|
143
|
+
Only what you actually send is captured: the service reads each app's transcript, which is written
|
|
144
|
+
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:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
graph-mind-forget # asks for the phrase without showing it, then confirms
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
It deletes every captured turn and memory containing the phrase from this PC and its indexes,
|
|
152
|
+
removes their lines from the shared memory, and tells your other PCs to delete their copies on
|
|
153
|
+
their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
|
|
154
|
+
(for Claude Code, `~/.claude/projects`) is separate and is not touched.
|
|
155
|
+
|
|
143
156
|
---
|
|
144
157
|
|
|
145
158
|
## MCP tools
|
|
@@ -36,7 +36,7 @@ import platform
|
|
|
36
36
|
import re
|
|
37
37
|
import sqlite3
|
|
38
38
|
|
|
39
|
-
KINDS = ("memory", "turn")
|
|
39
|
+
KINDS = ("memory", "turn", "forget") # forget: ids another PC erased (forget.py)
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def device_name() -> str:
|
|
@@ -426,6 +426,8 @@ def sync(shared: Path, index: Path) -> dict:
|
|
|
426
426
|
except ValueError:
|
|
427
427
|
continue # a corrupt line is skipped, not fatal
|
|
428
428
|
items.sort(key=lambda pair: pair[0].get("written_at", ""))
|
|
429
|
+
forgets = [item["record"] for item, _ in items if item.get("kind") == "forget"]
|
|
430
|
+
items = [(item, line) for item, line in items if item.get("kind") != "forget"]
|
|
429
431
|
imported, conflicts, waiting = 0, 0, []
|
|
430
432
|
with LocalBrainStore(index) as brain, ConversationMemoryStore(index) as turns:
|
|
431
433
|
# A supersession can arrive before the memory it replaces when two devices' logs are
|
|
@@ -450,6 +452,10 @@ def sync(shared: Path, index: Path) -> dict:
|
|
|
450
452
|
if not progress:
|
|
451
453
|
waiting = [line for _, line in retry]
|
|
452
454
|
break
|
|
455
|
+
if forgets: # after the imports: a forgotten turn may be in them
|
|
456
|
+
from forget import erase
|
|
457
|
+
for record in forgets:
|
|
458
|
+
erase(index, record)
|
|
453
459
|
with db:
|
|
454
460
|
db.execute("DELETE FROM brain_log_pending")
|
|
455
461
|
db.executemany("INSERT OR IGNORE INTO brain_log_pending VALUES(?)",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Forget everything that contains a phrase: on this PC, in the shared log, and on the other PCs.
|
|
2
|
+
|
|
3
|
+
graph-mind-forget # asks for the phrase without echoing it (keeps it out of
|
|
4
|
+
# the shell history), shows what matches, asks to confirm
|
|
5
|
+
graph-mind-forget "hunter2" --yes
|
|
6
|
+
|
|
7
|
+
A captured turn or memory lives in its store, in that store's word index, and as an engram in the
|
|
8
|
+
associative index; all three go. In the shared log its lines go too: Postgres rows are deleted, a
|
|
9
|
+
folder's lines are blanked in place (same length, so other PCs' read offsets stay valid). Then a
|
|
10
|
+
"forget" record listing the ids, never the phrase, tells every other PC to erase its own copies.
|
|
11
|
+
|
|
12
|
+
Not covered: the AI app's own transcript (Claude Code keeps ~/.claude/projects/*.jsonl) and the
|
|
13
|
+
embedding vectors, which are numbers keyed by a hash and hold no text.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import argparse
|
|
19
|
+
import getpass
|
|
20
|
+
import json
|
|
21
|
+
import sqlite3
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find(index: Path, phrase: str) -> dict:
|
|
26
|
+
"""Ids of the turns and memories whose text contains the phrase (exact, case-sensitive)."""
|
|
27
|
+
db = sqlite3.connect(index, timeout=30)
|
|
28
|
+
try:
|
|
29
|
+
tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master")}
|
|
30
|
+
turns = [row[0] for row in db.execute(
|
|
31
|
+
"SELECT turn_id FROM conversation_turns WHERE instr(content_redacted, ?) > 0",
|
|
32
|
+
(phrase,))] if "conversation_turns" in tables else []
|
|
33
|
+
memories = [row[0] for row in db.execute(
|
|
34
|
+
"SELECT memory_id FROM local_brain_memories "
|
|
35
|
+
"WHERE instr(content, ?) > 0 OR instr(title, ?) > 0",
|
|
36
|
+
(phrase, phrase))] if "local_brain_memories" in tables else []
|
|
37
|
+
finally:
|
|
38
|
+
db.close()
|
|
39
|
+
return {"turn_ids": turns, "memory_ids": memories}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def erase(index: Path, ids: dict) -> dict:
|
|
43
|
+
"""Delete these turns and memories, with every index entry derived from them."""
|
|
44
|
+
turns, memories = list(ids.get("turn_ids", [])), list(ids.get("memory_ids", []))
|
|
45
|
+
db = sqlite3.connect(index, timeout=30)
|
|
46
|
+
try:
|
|
47
|
+
tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master")}
|
|
48
|
+
with db:
|
|
49
|
+
if turns and "conversation_turns" in tables:
|
|
50
|
+
marks = ",".join("?" * len(turns))
|
|
51
|
+
rows = db.execute(f"SELECT revision, content_redacted FROM conversation_turns "
|
|
52
|
+
f"WHERE turn_id IN ({marks})", turns).fetchall()
|
|
53
|
+
# an external-content FTS table keeps its words until told to drop them
|
|
54
|
+
db.executemany("INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, "
|
|
55
|
+
"content_redacted) VALUES('delete', ?, ?)", rows)
|
|
56
|
+
db.execute(f"DELETE FROM conversation_turns WHERE turn_id IN ({marks})", turns)
|
|
57
|
+
if memories and "local_brain_memories" in tables:
|
|
58
|
+
marks = ",".join("?" * len(memories))
|
|
59
|
+
db.execute(f"DELETE FROM local_brain_memories WHERE memory_id IN ({marks})",
|
|
60
|
+
memories)
|
|
61
|
+
if "local_brain_memory_profiles" in tables:
|
|
62
|
+
db.execute(f"DELETE FROM local_brain_memory_profiles "
|
|
63
|
+
f"WHERE memory_id IN ({marks})", memories)
|
|
64
|
+
sources = turns + memories
|
|
65
|
+
if sources and "associative_engrams" in tables:
|
|
66
|
+
marks = ",".join("?" * len(sources))
|
|
67
|
+
engrams = [row[0] for row in db.execute(
|
|
68
|
+
f"SELECT engram_id FROM associative_engrams WHERE source_id IN ({marks})",
|
|
69
|
+
sources)]
|
|
70
|
+
if engrams:
|
|
71
|
+
em = ",".join("?" * len(engrams))
|
|
72
|
+
for table, column in (("associative_engrams_fts", "engram_id"),
|
|
73
|
+
("associative_terms", "engram_id"),
|
|
74
|
+
("associative_edges", "source_engram_id"),
|
|
75
|
+
("associative_edges", "target_engram_id"),
|
|
76
|
+
("associative_pending_edges", "source_engram_id"),
|
|
77
|
+
("associative_pending_edges", "target_engram_id"),
|
|
78
|
+
("associative_engrams", "engram_id")):
|
|
79
|
+
if table in tables:
|
|
80
|
+
db.execute(f"DELETE FROM {table} WHERE {column} IN ({em})", engrams)
|
|
81
|
+
finally:
|
|
82
|
+
db.close()
|
|
83
|
+
return {"turns": len(turns), "memories": len(memories)}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _keys(ids: dict) -> list[str]:
|
|
87
|
+
"""How a target's log line spells its id (brain_log writes sorted keys, ": " separators)."""
|
|
88
|
+
return ([f'"turn_id": {json.dumps(t)}' for t in ids.get("turn_ids", [])]
|
|
89
|
+
+ [f'"memory_id": {json.dumps(m)}' for m in ids.get("memory_ids", [])])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def scrub_shared(shared, ids: dict) -> int:
|
|
93
|
+
"""Remove the targets' lines from the shared log. Returns how many lines went."""
|
|
94
|
+
from brain_log import _pg, is_database
|
|
95
|
+
keys = _keys(ids)
|
|
96
|
+
if not keys:
|
|
97
|
+
return 0
|
|
98
|
+
if is_database(shared):
|
|
99
|
+
with _pg(shared) as connection:
|
|
100
|
+
removed = 0
|
|
101
|
+
for key in keys:
|
|
102
|
+
removed += connection.execute(
|
|
103
|
+
"DELETE FROM brain_log WHERE strpos(line, %s) > 0", (key,)).rowcount
|
|
104
|
+
connection.commit()
|
|
105
|
+
return removed
|
|
106
|
+
removed = 0
|
|
107
|
+
for log in (Path(shared) / "log").glob("*.jsonl"):
|
|
108
|
+
data = log.read_bytes()
|
|
109
|
+
lines = data.split(b"\n")
|
|
110
|
+
changed = False
|
|
111
|
+
for n, line in enumerate(lines):
|
|
112
|
+
text = line.decode("utf-8", "replace")
|
|
113
|
+
if any(key in text for key in keys) and '"kind": "forget"' not in text:
|
|
114
|
+
lines[n] = b" " * len(line) # same length: readers' offsets stay valid
|
|
115
|
+
changed, removed = True, removed + 1
|
|
116
|
+
if changed:
|
|
117
|
+
with log.open("r+b") as stream: # in place: the file is never shorter
|
|
118
|
+
stream.write(b"\n".join(lines))
|
|
119
|
+
return removed
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def forget(phrase: str, index: Path, shared=None) -> dict:
|
|
123
|
+
"""Find, erase here, scrub the shared log, and tell the other PCs."""
|
|
124
|
+
from brain_log import append
|
|
125
|
+
ids = find(index, phrase)
|
|
126
|
+
if not (ids["turn_ids"] or ids["memory_ids"]):
|
|
127
|
+
return {"turns": 0, "memories": 0, "shared_lines_removed": 0}
|
|
128
|
+
result = erase(index, ids)
|
|
129
|
+
if shared:
|
|
130
|
+
result["shared_lines_removed"] = scrub_shared(shared, ids)
|
|
131
|
+
append(shared, "forget", ids) # ids only: the phrase never leaves here
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv=None):
|
|
136
|
+
from brain_log import folder
|
|
137
|
+
from development_paths import default_development_db
|
|
138
|
+
parser = argparse.ArgumentParser(description="Forget every captured turn and memory "
|
|
139
|
+
"that contains a phrase, on every PC.")
|
|
140
|
+
parser.add_argument("phrase", nargs="?", help="omit it to type it without echo")
|
|
141
|
+
parser.add_argument("--yes", action="store_true", help="do not ask for confirmation")
|
|
142
|
+
args = parser.parse_args(argv)
|
|
143
|
+
phrase = args.phrase or getpass.getpass("Phrase to forget (not shown): ")
|
|
144
|
+
if len(phrase.strip()) < 4:
|
|
145
|
+
sys.exit("Use at least 4 characters, so a short word does not wipe half the memory.")
|
|
146
|
+
index = default_development_db()
|
|
147
|
+
ids = find(index, phrase)
|
|
148
|
+
count = len(ids["turn_ids"]) + len(ids["memory_ids"])
|
|
149
|
+
if not count:
|
|
150
|
+
print("Nothing in this PC's memory contains that phrase.")
|
|
151
|
+
return
|
|
152
|
+
print(f"{len(ids['turn_ids'])} conversation turns and {len(ids['memory_ids'])} memories "
|
|
153
|
+
"contain it.")
|
|
154
|
+
if not args.yes and input("Forget them here and on your other PCs? [y/N] ").lower() != "y":
|
|
155
|
+
print("Nothing changed.")
|
|
156
|
+
return
|
|
157
|
+
shared = folder()
|
|
158
|
+
result = forget(phrase, index, shared)
|
|
159
|
+
print(f"Forgotten: {result['turns']} turns, {result['memories']} memories"
|
|
160
|
+
+ (f"; {result['shared_lines_removed']} lines removed from the shared memory, "
|
|
161
|
+
"other PCs follow on their next sync" if shared else "") + ".")
|
|
162
|
+
print("Your AI app may still have it in its own history (Claude Code: ~/.claude/projects).")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-mind-memory
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -166,6 +166,19 @@ Then restart your AI apps. The server is also listed in the
|
|
|
166
166
|
Before anything is stored, these are masked: API keys, tokens from GitHub, AWS, Google and Slack,
|
|
167
167
|
private keys, and passwords inside URLs.
|
|
168
168
|
|
|
169
|
+
Only what you actually send is captured: the service reads each app's transcript, which is written
|
|
170
|
+
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:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
graph-mind-forget # asks for the phrase without showing it, then confirms
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It deletes every captured turn and memory containing the phrase from this PC and its indexes,
|
|
178
|
+
removes their lines from the shared memory, and tells your other PCs to delete their copies on
|
|
179
|
+
their next sync. Only ids are shared for that, never the phrase. Your AI app's own history
|
|
180
|
+
(for Claude Code, `~/.claude/projects`) is separate and is not touched.
|
|
181
|
+
|
|
169
182
|
---
|
|
170
183
|
|
|
171
184
|
## MCP tools
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graph-mind-memory"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
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"
|
|
@@ -39,6 +39,7 @@ Issues = "https://github.com/goyohan0611-png/graph-mind/issues"
|
|
|
39
39
|
graph-mind = "graph_mind_mcp_server:main"
|
|
40
40
|
graph-mind-install = "install:main"
|
|
41
41
|
graph-mind-capture = "automatic_capture_cli:main"
|
|
42
|
+
graph-mind-forget = "forget:main"
|
|
42
43
|
|
|
43
44
|
[tool.setuptools]
|
|
44
45
|
# ponytail: flat modules, as in the repository; a graph_mind/ package is the upgrade path.
|
|
@@ -54,6 +55,7 @@ py-modules = [
|
|
|
54
55
|
"development_paths",
|
|
55
56
|
"embedding_warmup",
|
|
56
57
|
"entity_timeline",
|
|
58
|
+
"forget",
|
|
57
59
|
"graph_mind_mcp_server",
|
|
58
60
|
"install",
|
|
59
61
|
"local_brain",
|
|
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.2.0 → graph_mind_memory-0.2.1}/graph_mind_memory.egg-info/dependency_links.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
|
|
File without changes
|
|
File without changes
|