memdebug 0.2.0__py3-none-any.whl
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.
- memdebug/__init__.py +3 -0
- memdebug/__main__.py +3 -0
- memdebug/adapters/__init__.py +0 -0
- memdebug/adapters/base.py +37 -0
- memdebug/adapters/common.py +69 -0
- memdebug/adapters/folder.py +96 -0
- memdebug/adapters/markdown_git.py +731 -0
- memdebug/adapters/mem0.py +324 -0
- memdebug/adapters/openwebui.py +209 -0
- memdebug/adapters/restore.py +705 -0
- memdebug/agents.py +128 -0
- memdebug/backends.py +29 -0
- memdebug/cli.py +857 -0
- memdebug/demo.py +190 -0
- memdebug/describe.py +34 -0
- memdebug/diff.py +91 -0
- memdebug/docker_source.py +164 -0
- memdebug/errors.py +29 -0
- memdebug/hints.py +145 -0
- memdebug/ledger.py +700 -0
- memdebug/models.py +157 -0
- memdebug/monitor.py +246 -0
- memdebug/paths.py +19 -0
- memdebug/reconcile.py +83 -0
- memdebug/report.py +279 -0
- memdebug/rollback_flow.py +69 -0
- memdebug/selftest.py +482 -0
- memdebug/stores.py +276 -0
- memdebug/sync.py +177 -0
- memdebug/textsafe.py +73 -0
- memdebug/viewer/__init__.py +1 -0
- memdebug/viewer/html.py +136 -0
- memdebug/viewer/pages.py +569 -0
- memdebug/viewer/redline.py +242 -0
- memdebug/viewer/server.py +432 -0
- memdebug/viewer/style.py +218 -0
- memdebug/witness.py +199 -0
- memdebug-0.2.0.dist-info/METADATA +206 -0
- memdebug-0.2.0.dist-info/RECORD +43 -0
- memdebug-0.2.0.dist-info/WHEEL +4 -0
- memdebug-0.2.0.dist-info/entry_points.txt +2 -0
- memdebug-0.2.0.dist-info/licenses/LICENSE +202 -0
- memdebug-0.2.0.dist-info/licenses/NOTICE +4 -0
memdebug/agents.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Which agents are on this computer, and where each keeps what it remembers.
|
|
2
|
+
|
|
3
|
+
Each entry below was checked against the agent's own documentation. Presence is judged only by whether a known folder exists
|
|
4
|
+
(`lstat`; nothing is opened or read, links and junctions are ignored). Where an agent keeps memory in one file inside a folder
|
|
5
|
+
that also holds settings or credentials (`~/.gemini`, `~/.codex`, `~/.claude`), only that file is offered, never the folder.
|
|
6
|
+
|
|
7
|
+
Assistants that keep their memory in the provider's cloud (ChatGPT, Claude's apps, Gemini, Copilot) have nothing on this computer
|
|
8
|
+
to watch, and memdebug says so instead of pretending: it cannot tell whether those apps are installed, and it could not read
|
|
9
|
+
their memory if they were.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import stat
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .adapters.markdown_git import _is_reparse_point
|
|
19
|
+
from .stores import Candidate, discover
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Place:
|
|
24
|
+
parts: tuple[str, ...] # folder, relative to the home folder
|
|
25
|
+
files: tuple[str, ...] = () # if set: only these files in that folder, nothing else
|
|
26
|
+
name: str = "" # suggested store name
|
|
27
|
+
what: str = ""
|
|
28
|
+
always: bool = False # offer even before any note exists (the agent creates the folder itself)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Agent:
|
|
33
|
+
key: str
|
|
34
|
+
name: str
|
|
35
|
+
sign: tuple[str, ...] # a folder under home whose existence means the agent is probably installed
|
|
36
|
+
note: str
|
|
37
|
+
places: tuple[Place, ...] = ()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
AGENTS: tuple[Agent, ...] = (
|
|
41
|
+
Agent("claude-code", "Claude Code", (".claude",),
|
|
42
|
+
"keeps notes in CLAUDE.md files and a memory folder per project",
|
|
43
|
+
(Place((".claude",), ("CLAUDE.md",), "claude-code-global", "your global CLAUDE.md instructions"),)),
|
|
44
|
+
Agent("openclaw", "OpenClaw", (".openclaw",),
|
|
45
|
+
"keeps its memory as markdown in a workspace folder (MEMORY.md, memory/, USER.md); its documentation suggests keeping it in git",
|
|
46
|
+
(Place((".openclaw", "workspace"), (), "openclaw", "the OpenClaw workspace (memory, identity and instruction files)", always=True),)),
|
|
47
|
+
Agent("gemini-cli", "Gemini CLI", (".gemini",),
|
|
48
|
+
"saves what it is asked to remember in ~/.gemini/GEMINI.md",
|
|
49
|
+
(Place((".gemini",), ("GEMINI.md",), "gemini-cli", "GEMINI.md, where its memory tool saves facts", always=True),)),
|
|
50
|
+
Agent("codex-cli", "Codex CLI", (".codex",),
|
|
51
|
+
"has no memory of its own; it follows the instructions in ~/.codex/AGENTS.md, which is why planted text there matters",
|
|
52
|
+
(Place((".codex",), ("AGENTS.md", "AGENTS.override.md"), "codex-cli", "its AGENTS.md instruction files"),)),
|
|
53
|
+
Agent("windsurf", "Windsurf", (".codeium", "windsurf"),
|
|
54
|
+
"stores its automatically generated memories on this computer only",
|
|
55
|
+
(Place((".codeium", "windsurf", "memories"), (), "windsurf", "Cascade's memories (the markdown files in it)"),)),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
CLOUD_NOTE = ("ChatGPT, Claude (claude.ai and its desktop and phone apps), Gemini and Copilot keep their memory in the provider's cloud. "
|
|
59
|
+
"memdebug cannot see it, and cannot tell whether those apps are installed. Review or clear it in each app's settings; "
|
|
60
|
+
"to keep a record of how it changes, copy it into a markdown file now and then, in a folder you add to memdebug.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class FoundAgent:
|
|
65
|
+
agent: Agent
|
|
66
|
+
candidates: list[Candidate] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _real_folder(path: Path) -> bool:
|
|
70
|
+
try:
|
|
71
|
+
info = os.lstat(path)
|
|
72
|
+
except OSError:
|
|
73
|
+
return False
|
|
74
|
+
return stat.S_ISDIR(info.st_mode) and not stat.S_ISLNK(info.st_mode) and not _is_reparse_point(info)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _has_markdown(path: Path) -> bool:
|
|
78
|
+
try:
|
|
79
|
+
return any(name.lower().endswith(".md") for name in os.listdir(path)[:2000])
|
|
80
|
+
except OSError:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_plain_file(path: Path) -> bool:
|
|
85
|
+
try:
|
|
86
|
+
info = os.lstat(path)
|
|
87
|
+
except OSError:
|
|
88
|
+
return False
|
|
89
|
+
return stat.S_ISREG(info.st_mode) and not _is_reparse_point(info)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def scan_agents(home: Path | None = None) -> list[FoundAgent]:
|
|
93
|
+
"""The known agents that appear to be installed, with what could be watched for each. Reads no file contents."""
|
|
94
|
+
root = home or Path.home()
|
|
95
|
+
found: list[FoundAgent] = []
|
|
96
|
+
for agent in AGENTS:
|
|
97
|
+
if not _real_folder(root.joinpath(*agent.sign)):
|
|
98
|
+
continue
|
|
99
|
+
entry = FoundAgent(agent)
|
|
100
|
+
for place in agent.places:
|
|
101
|
+
folder = root.joinpath(*place.parts)
|
|
102
|
+
if not _real_folder(folder):
|
|
103
|
+
continue
|
|
104
|
+
if place.files:
|
|
105
|
+
present = [name for name in place.files if _is_plain_file(folder / name)]
|
|
106
|
+
if not present and not place.always:
|
|
107
|
+
continue
|
|
108
|
+
entry.candidates.append(Candidate("folder", place.name, folder, f"{agent.name}: {place.what}", files=place.files))
|
|
109
|
+
elif place.always or _has_markdown(folder):
|
|
110
|
+
entry.candidates.append(Candidate("folder", place.name, folder, f"{agent.name}: {place.what}"))
|
|
111
|
+
if agent.key == "claude-code": # its per-project memory folders are found by name, as before
|
|
112
|
+
entry.candidates += [Candidate(c.kind, c.name, c.path, f"{agent.name}: {c.why}") for c in discover(root)]
|
|
113
|
+
found.append(entry)
|
|
114
|
+
return found
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def summary_lines(found: list[FoundAgent], docker_names: list[str] | None = None) -> list[str]:
|
|
118
|
+
lines = []
|
|
119
|
+
for item in found:
|
|
120
|
+
watchable = f"{len(item.candidates)} place(s) memdebug can watch" if item.candidates else "nothing to watch yet"
|
|
121
|
+
lines.append(f" {item.agent.name}: {item.agent.note} ({watchable})")
|
|
122
|
+
for container in docker_names or []:
|
|
123
|
+
lines.append(f" Open WebUI (running in Docker as '{container}'): keeps its memory in a database (1 place memdebug can watch)")
|
|
124
|
+
if not lines:
|
|
125
|
+
lines.append(" None of the agents memdebug knows were found.")
|
|
126
|
+
lines.append("")
|
|
127
|
+
lines.append(f" {CLOUD_NOTE}")
|
|
128
|
+
return lines
|
memdebug/backends.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Small facts about store types that the viewer and the command line both need."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .textsafe import safe_text
|
|
7
|
+
|
|
8
|
+
# Stores that keep no change history of their own: memdebug can only record what it sees between two looks, so it cannot say
|
|
9
|
+
# who made a change or whether it bypassed anything.
|
|
10
|
+
HISTORYLESS = frozenset({"folder", "openwebui"})
|
|
11
|
+
|
|
12
|
+
_OPAQUE = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}|[0-9a-fA-F]{32}|[0-9a-fA-F]{40}|[0-9a-fA-F]{64}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_opaque_id(memory_id: str) -> bool:
|
|
16
|
+
"""A random identifier (a UUID or a hash) that tells a person nothing, as opposed to a file name or a short id."""
|
|
17
|
+
return _OPAQUE.fullmatch(memory_id) is not None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def short_id(memory_id: str) -> str:
|
|
21
|
+
return memory_id[:8] + "…" if is_opaque_id(memory_id) else memory_id
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def friendly_id(memory_id: str, text: str | None, width: int = 40) -> str:
|
|
25
|
+
"""For an opaque id: its short form and the start of what the memory says. Otherwise the id itself, which is already meaningful."""
|
|
26
|
+
if not is_opaque_id(memory_id):
|
|
27
|
+
return safe_text(memory_id, 60)
|
|
28
|
+
words = safe_text(" ".join((text or "").split()), width)
|
|
29
|
+
return f'{memory_id[:8]}… "{words}"' if words else f"{memory_id[:8]}…"
|