agent-memory-git 0.1.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.
- agent_memory/PROTOCOL.md +80 -0
- agent_memory/__init__.py +1 -0
- agent_memory/__main__.py +7 -0
- agent_memory/capture.py +207 -0
- agent_memory/cli.py +317 -0
- agent_memory/config.py +53 -0
- agent_memory/context.py +73 -0
- agent_memory/embed_worker.py +90 -0
- agent_memory/frontmatter.py +146 -0
- agent_memory/gitsync.py +119 -0
- agent_memory/hooks.py +95 -0
- agent_memory/importers.py +231 -0
- agent_memory/install.py +483 -0
- agent_memory/lint.py +45 -0
- agent_memory/select.py +105 -0
- agent_memory/sessions.py +64 -0
- agent_memory/store.py +97 -0
- agent_memory/ui.py +410 -0
- agent_memory/ui_server.py +371 -0
- agent_memory/util.py +101 -0
- agent_memory/webui/app.css +192 -0
- agent_memory/webui/app.js +635 -0
- agent_memory/webui/index.html +31 -0
- agent_memory/workdir.py +135 -0
- agent_memory_git-0.1.0.dist-info/METADATA +183 -0
- agent_memory_git-0.1.0.dist-info/RECORD +29 -0
- agent_memory_git-0.1.0.dist-info/WHEEL +4 -0
- agent_memory_git-0.1.0.dist-info/entry_points.txt +3 -0
- agent_memory_git-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""One-off import of the native Claude Code and Codex memories.
|
|
2
|
+
|
|
3
|
+
Native files are only read, never modified. Re-running an import skips
|
|
4
|
+
memories that were already imported (matched on `source.original`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import frontmatter, gitsync, sessions, workdir
|
|
15
|
+
from .context import detect
|
|
16
|
+
from .store import load_all, memories_dir, normalize, read
|
|
17
|
+
from .util import HOME, atomic_write, id_path, new_id, now, parse_time, slugify
|
|
18
|
+
|
|
19
|
+
CLAUDE_PROJECTS = Path(os.environ.get("CLAUDE_CONFIG_DIR", HOME / ".claude")) / "projects"
|
|
20
|
+
CODEX_MEMORIES = Path(os.environ.get("CODEX_HOME", HOME / ".codex")) / "memories"
|
|
21
|
+
ADHOC_TAG = "[ad-hoc note]"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _git_source(cwd):
|
|
25
|
+
if not cwd:
|
|
26
|
+
return {}
|
|
27
|
+
ctx = detect(cwd) if os.path.isdir(cwd) else {"cwd": cwd, "host": detect(HOME)["host"]}
|
|
28
|
+
ctx.pop("git_commit", None) # the commit at import time says nothing about the memory
|
|
29
|
+
return ctx
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _record(*, name, typ, scope, description, body, created, source):
|
|
33
|
+
when = parse_time(created) or now()
|
|
34
|
+
memory_id = new_id(name, when)
|
|
35
|
+
meta = {"id": memory_id, "name": slugify(name), "type": typ, "scope": scope,
|
|
36
|
+
"description": " ".join(description.split())[:300], "created": when.isoformat(),
|
|
37
|
+
"source": {k: v for k, v in source.items() if v}}
|
|
38
|
+
return memory_id, meta, body
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------- Claude Code
|
|
42
|
+
|
|
43
|
+
def _decode_project_dir(name):
|
|
44
|
+
"""Recover a folder from Claude's dash-encoded project dir name, if it still exists."""
|
|
45
|
+
tokens = name.lstrip("-").split("-")
|
|
46
|
+
|
|
47
|
+
@lru_cache(maxsize=None)
|
|
48
|
+
def entries(base):
|
|
49
|
+
try:
|
|
50
|
+
return tuple(os.listdir(base))
|
|
51
|
+
except OSError:
|
|
52
|
+
return ()
|
|
53
|
+
|
|
54
|
+
def walk(i, base, comp):
|
|
55
|
+
if i == len(tokens):
|
|
56
|
+
return str(base / comp) if (base / comp).exists() else None
|
|
57
|
+
tok = tokens[i]
|
|
58
|
+
if (base / comp).is_dir():
|
|
59
|
+
hit = walk(i + 1, base / comp, tok)
|
|
60
|
+
if hit:
|
|
61
|
+
return hit
|
|
62
|
+
for sep in ("-", ".", "_", " "):
|
|
63
|
+
longer = comp + sep + tok
|
|
64
|
+
if any(e.startswith(longer) for e in entries(base)):
|
|
65
|
+
hit = walk(i + 1, base, longer)
|
|
66
|
+
if hit:
|
|
67
|
+
return hit
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
return walk(1, Path("/"), tokens[0]) if tokens else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _transcript_cwd(project_dir, session_id):
|
|
74
|
+
candidates = []
|
|
75
|
+
if session_id:
|
|
76
|
+
candidates.append(project_dir / f"{session_id}.jsonl")
|
|
77
|
+
candidates += sorted(project_dir.glob("*.jsonl"))
|
|
78
|
+
for path in candidates:
|
|
79
|
+
if not path.exists():
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
82
|
+
with path.open(encoding="utf-8", errors="replace") as f:
|
|
83
|
+
for raw in f:
|
|
84
|
+
if '"cwd"' in raw:
|
|
85
|
+
cwd = json.loads(raw).get("cwd")
|
|
86
|
+
if cwd:
|
|
87
|
+
return cwd, path
|
|
88
|
+
except (OSError, ValueError):
|
|
89
|
+
continue
|
|
90
|
+
return None, None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def claude_items():
|
|
94
|
+
for mem_dir in sorted(CLAUDE_PROJECTS.glob("*/memory")):
|
|
95
|
+
project_dir = mem_dir.parent
|
|
96
|
+
for path in sorted(mem_dir.glob("*.md")):
|
|
97
|
+
if path.name == "MEMORY.md":
|
|
98
|
+
continue
|
|
99
|
+
meta, body = read(path)
|
|
100
|
+
legacy = meta.get("metadata") if isinstance(meta.get("metadata"), dict) else {}
|
|
101
|
+
m = normalize(meta, body, path.stem)
|
|
102
|
+
origin = legacy.get("originSessionId") or meta.get("originSessionId")
|
|
103
|
+
cwd, transcript = _transcript_cwd(project_dir, origin)
|
|
104
|
+
cwd = cwd or _decode_project_dir(project_dir.name)
|
|
105
|
+
original_cwd = None
|
|
106
|
+
if workdir.is_throwaway(cwd):
|
|
107
|
+
inferred = workdir.infer(transcript, cwd, workdir.default_exclude())
|
|
108
|
+
if inferred:
|
|
109
|
+
original_cwd, cwd = cwd, inferred
|
|
110
|
+
created = legacy.get("modified") or meta.get("modified") or _mtime(path)
|
|
111
|
+
title = sessions.title("claude-code", origin, transcript) if transcript else ""
|
|
112
|
+
source = {"tool": "claude-code", "session_id": origin, "session_name": title,
|
|
113
|
+
**_git_source(cwd),
|
|
114
|
+
"original_cwd": original_cwd,
|
|
115
|
+
"imported_from": "claude-code auto memory",
|
|
116
|
+
"original": "~/" + path.relative_to(HOME).as_posix()}
|
|
117
|
+
yield _record(name=m["name"] or path.stem, typ=m["type"],
|
|
118
|
+
scope="global" if m["type"] in ("user", "feedback") else "here",
|
|
119
|
+
description=m["description"], body=body, created=created,
|
|
120
|
+
source=source)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _mtime(path):
|
|
124
|
+
return datetime.fromtimestamp(path.stat().st_mtime).astimezone().isoformat()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------- Codex
|
|
128
|
+
|
|
129
|
+
def _sections(text):
|
|
130
|
+
out, current = {}, None
|
|
131
|
+
for line in text.splitlines():
|
|
132
|
+
if line.startswith("## "):
|
|
133
|
+
current = line[3:].strip()
|
|
134
|
+
out[current] = []
|
|
135
|
+
elif current:
|
|
136
|
+
out[current].append(line)
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _bullets(lines):
|
|
141
|
+
items, cur = [], None
|
|
142
|
+
for line in lines:
|
|
143
|
+
if line.startswith("- "):
|
|
144
|
+
cur = [line[2:].strip()]
|
|
145
|
+
items.append(cur)
|
|
146
|
+
elif cur is not None and line.startswith(" ") and line.strip():
|
|
147
|
+
cur.append(line.strip())
|
|
148
|
+
else:
|
|
149
|
+
cur = None
|
|
150
|
+
return [" ".join(i) for i in items]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _codex_source(original, cwd=None, thread=None):
|
|
154
|
+
title = sessions.title("codex", thread) if thread else ""
|
|
155
|
+
original_cwd = None
|
|
156
|
+
if thread and workdir.is_throwaway(cwd):
|
|
157
|
+
rollout = sessions.transcript_path("codex", thread)
|
|
158
|
+
inferred = workdir.infer(rollout, cwd, workdir.default_exclude())
|
|
159
|
+
if inferred:
|
|
160
|
+
original_cwd, cwd = cwd, inferred
|
|
161
|
+
return {"tool": "codex", "session_id": thread, "session_name": title,
|
|
162
|
+
**_git_source(cwd), "original_cwd": original_cwd, "imported_from": "codex memories",
|
|
163
|
+
"original": "~/" + (CODEX_MEMORIES / original).relative_to(HOME).as_posix()}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def codex_items():
|
|
167
|
+
summary = CODEX_MEMORIES / "memory_summary.md"
|
|
168
|
+
if summary.exists():
|
|
169
|
+
sec = _sections(summary.read_text("utf-8"))
|
|
170
|
+
created = _mtime(summary)
|
|
171
|
+
profile = " ".join(line.strip() for line in sec.get("User Profile", []) if line.strip())
|
|
172
|
+
if profile:
|
|
173
|
+
profile = profile.replace(ADHOC_TAG, "").strip()
|
|
174
|
+
yield _record(name="user-profile-from-codex", typ="user", scope="global",
|
|
175
|
+
description="User profile as summarised by Codex: " + profile[:200],
|
|
176
|
+
body=profile + "\n", created=created,
|
|
177
|
+
source=_codex_source("memory_summary.md#user-profile"))
|
|
178
|
+
for heading in ("User preferences", "General Tips"):
|
|
179
|
+
for n, text in enumerate(_bullets(sec.get(heading, []))):
|
|
180
|
+
if ADHOC_TAG in text or text.startswith("Skills:"):
|
|
181
|
+
continue # ad-hoc notes are imported from their own files
|
|
182
|
+
yield _record(name=" ".join(text.split()[:6]), typ="feedback", scope="global",
|
|
183
|
+
description=text, body=text + "\n", created=created,
|
|
184
|
+
source=_codex_source(f"memory_summary.md#{slugify(heading)}-{n}"))
|
|
185
|
+
|
|
186
|
+
handbook = CODEX_MEMORIES / "MEMORY.md"
|
|
187
|
+
if handbook.exists():
|
|
188
|
+
blocks = re.split(r"(?m)^(?=# Task Group: )", handbook.read_text("utf-8"))
|
|
189
|
+
for block in blocks:
|
|
190
|
+
if not block.startswith("# Task Group: "):
|
|
191
|
+
continue
|
|
192
|
+
title = block.splitlines()[0][len("# Task Group: "):].strip()
|
|
193
|
+
cwd = next(iter(re.findall(r"cwd=([^\s;,)]+)", block)), None)
|
|
194
|
+
thread = next(iter(re.findall(r"thread_id=([0-9a-f-]{36})", block)), None)
|
|
195
|
+
stamps = sorted(re.findall(r"updated_at=([0-9T:.+-]+Z?)", block))
|
|
196
|
+
yield _record(name=title, typ="project", scope="here",
|
|
197
|
+
description=f"Codex task history: {title}", body=block.rstrip() + "\n",
|
|
198
|
+
created=stamps[-1] if stamps else _mtime(handbook),
|
|
199
|
+
source=_codex_source(f"MEMORY.md#{slugify(title)}", cwd, thread))
|
|
200
|
+
|
|
201
|
+
for path in sorted((CODEX_MEMORIES / "extensions" / "ad_hoc" / "notes").glob("*.md")):
|
|
202
|
+
body = path.read_text("utf-8")
|
|
203
|
+
_, raw = frontmatter.split(body)
|
|
204
|
+
desc = normalize({}, raw, path.stem)["description"]
|
|
205
|
+
name = re.sub(r"^[0-9T_-]+", "", path.stem) or path.stem
|
|
206
|
+
yield _record(name=name, typ="reference", scope="global", description=desc, body=raw,
|
|
207
|
+
created=_mtime(path),
|
|
208
|
+
source=_codex_source(path.relative_to(CODEX_MEMORIES).as_posix()))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------- driver
|
|
212
|
+
|
|
213
|
+
def run(which, data_dir, dry_run=False):
|
|
214
|
+
items = list(claude_items() if which == "claude" else codex_items())
|
|
215
|
+
seen = {m["source"].get("original") for m in load_all(data_dir)}
|
|
216
|
+
fresh = [it for it in items if it[1]["source"].get("original") not in seen]
|
|
217
|
+
for _, meta, _ in fresh:
|
|
218
|
+
src = meta["source"]
|
|
219
|
+
where = src.get("git_remote") or src.get("cwd") or "-"
|
|
220
|
+
print(f"{meta['type']:<9} {meta['scope']:<6} {meta['description'][:70]:<70} {where}")
|
|
221
|
+
print(f"\n{len(fresh)} to import, {len(items) - len(fresh)} already imported")
|
|
222
|
+
if dry_run or not fresh:
|
|
223
|
+
return
|
|
224
|
+
with gitsync.Lock(data_dir, timeout=30) as locked:
|
|
225
|
+
if not locked:
|
|
226
|
+
raise SystemExit("lock busy")
|
|
227
|
+
for memory_id, meta, body in fresh:
|
|
228
|
+
atomic_write(memories_dir(data_dir) / id_path(memory_id), frontmatter.join(meta, body))
|
|
229
|
+
label = "claude-code auto memory" if which == "claude" else "codex memories"
|
|
230
|
+
gitsync.commit(data_dir, f"import: {len(fresh)} memories from {label}\n\nTool: import")
|
|
231
|
+
print("imported and committed")
|