agent-memory-git 0.1.1__tar.gz → 0.1.3__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.
Files changed (31) hide show
  1. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/PKG-INFO +10 -5
  2. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/README.md +9 -4
  3. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/pyproject.toml +1 -1
  4. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/pyproject.toml.orig +1 -1
  5. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/capture.py +4 -4
  6. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/cli.py +17 -10
  7. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/context.py +1 -2
  8. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/frontmatter.py +2 -3
  9. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/gitsync.py +11 -5
  10. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/hooks.py +23 -14
  11. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/importers.py +76 -30
  12. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/install.py +62 -30
  13. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/lint.py +9 -3
  14. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/select.py +1 -2
  15. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/sessions.py +1 -2
  16. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/ui.py +109 -53
  17. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/ui_server.py +70 -23
  18. agent_memory_git-0.1.3/src/agent_memory/webui/app.css +263 -0
  19. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/webui/app.js +180 -76
  20. agent_memory_git-0.1.3/src/agent_memory/webui/index.html +47 -0
  21. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/workdir.py +10 -7
  22. agent_memory_git-0.1.1/src/agent_memory/webui/app.css +0 -192
  23. agent_memory_git-0.1.1/src/agent_memory/webui/index.html +0 -31
  24. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/LICENSE +0 -0
  25. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/PROTOCOL.md +0 -0
  26. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/__init__.py +0 -0
  27. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/__main__.py +0 -0
  28. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/config.py +0 -0
  29. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/embed_worker.py +0 -0
  30. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/store.py +0 -0
  31. {agent_memory_git-0.1.1 → agent_memory_git-0.1.3}/src/agent_memory/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-memory-git
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Shared, git-backed memory for Claude Code and Codex.
5
5
  Keywords: claude-code,codex,memory,agents,git
6
6
  Author: Shichao Song
@@ -47,6 +47,8 @@ embedding model; leave it out after `mem ui --fetch-model` to also search by mea
47
47
 
48
48
  ## How it works
49
49
 
50
+ ![mem install wires hooks into Claude Code and Codex; at session start they load an index from the private data repository, and at the end of each turn they commit and push new memories back](https://raw.githubusercontent.com/Ki-Seki/agent-memory/main/docs/how-it-works.svg)
51
+
50
52
  - **One file per memory.** Frontmatter carries `type` (user / feedback / project / reference),
51
53
  `scope` (global / here), a one-line `description`, and provenance under `source`: tool, session
52
54
  id and name, model, host, folder, git root, remote, branch and commit. Files live in
@@ -129,11 +131,13 @@ terminal open while you use it. It needs `numpy`; the hooks do not.
129
131
  - **Find.** One search box (press `/`). Word matches appear as you type, with the matching words
130
132
  marked; a moment later the local model adds memories close in meaning, across Chinese and
131
133
  English. Filters narrow the list by type, where a memory loads (everywhere, or one project or
132
- folder), which tool wrote it, keyword, age, and whether it needs tidying (what `mem lint`
133
- reports, also listed on the open memory); the order is newest, oldest, recently changed or A to Z.
134
+ folder), which tool wrote it, keyword, when it was added (a recent period or a date range), and
135
+ status (needs tidying, as `mem lint` reports and the open memory lists, or unchanged for 60
136
+ days). Tick several values in one filter to see memories that match any of them. The date
137
+ column's header sets the order: newest, oldest, recently changed or A to Z.
134
138
  - **Read.** Click a row, or move with `j`/`k` and press Enter. The memory opens on the right with
135
- every frontmatter field (including each `source` entry), the body rendered as Markdown with
136
- `[[links]]` that open other memories, the file exactly as stored, and the most similar memories.
139
+ the body rendered as Markdown with `[[links]]` that open other memories, every frontmatter field
140
+ (including each `source` entry), the most similar memories, and the file exactly as stored.
137
141
  - **Change.** Edit (`e`) or delete from the open memory, add one with New (`n`), or tick several
138
142
  rows (`x`) and delete them together. Every change is one git commit, pushed in the background,
139
143
  with Undo. If an agent edits the same file meanwhile, saving is refused rather than overwriting.
@@ -157,6 +161,7 @@ prints, so other web pages cannot read or change memories.
157
161
  uv sync --all-extras # dev tools and numpy
158
162
  uv run pytest
159
163
  uv run ruff check .
164
+ uv run ruff format .
160
165
  ```
161
166
 
162
167
  `uv run mem ...` runs the working copy without touching the installed one. To have every
@@ -27,6 +27,8 @@ embedding model; leave it out after `mem ui --fetch-model` to also search by mea
27
27
 
28
28
  ## How it works
29
29
 
30
+ ![mem install wires hooks into Claude Code and Codex; at session start they load an index from the private data repository, and at the end of each turn they commit and push new memories back](https://raw.githubusercontent.com/Ki-Seki/agent-memory/main/docs/how-it-works.svg)
31
+
30
32
  - **One file per memory.** Frontmatter carries `type` (user / feedback / project / reference),
31
33
  `scope` (global / here), a one-line `description`, and provenance under `source`: tool, session
32
34
  id and name, model, host, folder, git root, remote, branch and commit. Files live in
@@ -109,11 +111,13 @@ terminal open while you use it. It needs `numpy`; the hooks do not.
109
111
  - **Find.** One search box (press `/`). Word matches appear as you type, with the matching words
110
112
  marked; a moment later the local model adds memories close in meaning, across Chinese and
111
113
  English. Filters narrow the list by type, where a memory loads (everywhere, or one project or
112
- folder), which tool wrote it, keyword, age, and whether it needs tidying (what `mem lint`
113
- reports, also listed on the open memory); the order is newest, oldest, recently changed or A to Z.
114
+ folder), which tool wrote it, keyword, when it was added (a recent period or a date range), and
115
+ status (needs tidying, as `mem lint` reports and the open memory lists, or unchanged for 60
116
+ days). Tick several values in one filter to see memories that match any of them. The date
117
+ column's header sets the order: newest, oldest, recently changed or A to Z.
114
118
  - **Read.** Click a row, or move with `j`/`k` and press Enter. The memory opens on the right with
115
- every frontmatter field (including each `source` entry), the body rendered as Markdown with
116
- `[[links]]` that open other memories, the file exactly as stored, and the most similar memories.
119
+ the body rendered as Markdown with `[[links]]` that open other memories, every frontmatter field
120
+ (including each `source` entry), the most similar memories, and the file exactly as stored.
117
121
  - **Change.** Edit (`e`) or delete from the open memory, add one with New (`n`), or tick several
118
122
  rows (`x`) and delete them together. Every change is one git commit, pushed in the background,
119
123
  with Undo. If an agent edits the same file meanwhile, saving is refused rather than overwriting.
@@ -137,6 +141,7 @@ prints, so other web pages cannot read or change memories.
137
141
  uv sync --all-extras # dev tools and numpy
138
142
  uv run pytest
139
143
  uv run ruff check .
144
+ uv run ruff format .
140
145
  ```
141
146
 
142
147
  `uv run mem ...` runs the working copy without touching the installed one. To have every
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -148,10 +148,10 @@ def stamp_updates(data_dir):
148
148
 
149
149
  def commit(data_dir, session, label=""):
150
150
  stamp_updates(data_dir)
151
- title = sessions.title(session.get("tool"), session.get("session_id"),
152
- session.get("transcript_path")) or session.get("session_title", "")
153
- stat = gitsync.git(data_dir, "status", "--porcelain", "-uall", "--",
154
- "memories").stdout.splitlines()
151
+ title = sessions.title(
152
+ session.get("tool"), session.get("session_id"), session.get("transcript_path")
153
+ ) or session.get("session_title", "")
154
+ stat = gitsync.git(data_dir, "status", "--porcelain", "-uall", "--", "memories").stdout.splitlines()
155
155
  added = sum(1 for s in stat if s.startswith("??") or s.startswith("A"))
156
156
  deleted = sum(1 for s in stat if s[:2].strip() == "D")
157
157
  changed = len(stat) - added - deleted
@@ -74,8 +74,7 @@ def cmd_show(args):
74
74
  def cmd_index(args):
75
75
  data = _data()
76
76
  ctx = detect(args.cwd)
77
- paths = {"memories": str(store.memories_dir(data)), "staging": "(per session)",
78
- "mem": str(config.BIN)}
77
+ paths = {"memories": str(store.memories_dir(data)), "staging": "(per session)", "mem": str(config.BIN)}
79
78
  text = select.render(store.load_all(data), ctx, paths)
80
79
  print(text, end="")
81
80
  print(f"\n[{len(text)} characters]", file=sys.stderr)
@@ -88,8 +87,7 @@ def cmd_add(args):
88
87
  sdir = store.staging_dir(data) / f"cli-{slugify(args.name)}"
89
88
  sdir.mkdir(parents=True, exist_ok=True)
90
89
  capture.write_session(sdir, session)
91
- meta = {"name": args.name, "type": args.type, "scope": args.scope,
92
- "description": args.description}
90
+ meta = {"name": args.name, "type": args.type, "scope": args.scope, "description": args.description}
93
91
  atomic_write(sdir / f"{slugify(args.name)}.md", frontmatter.join(meta, body))
94
92
  capture.flush(data, sdir, final=True)
95
93
  print("saved")
@@ -107,6 +105,7 @@ def cmd_flush(args):
107
105
  def cmd_relocate(args):
108
106
  """Re-attribute existing memories recorded in throwaway folders."""
109
107
  from . import sessions, workdir
108
+
110
109
  data = _data()
111
110
  mdir = store.memories_dir(data)
112
111
  moved = 0
@@ -116,8 +115,7 @@ def cmd_relocate(args):
116
115
  continue
117
116
  if args.only and not any(o in m["rel"] for o in args.only):
118
117
  continue
119
- transcript = src.get("transcript") or sessions.transcript_path(src.get("tool"),
120
- src.get("session_id"))
118
+ transcript = src.get("transcript") or sessions.transcript_path(src.get("tool"), src.get("session_id"))
121
119
  ctx = capture.relocate({"cwd": src["cwd"], "host": src.get("host")}, transcript)
122
120
  if not ctx.get("original_cwd"):
123
121
  print(f"keep {m['rel']} (no project found)")
@@ -146,12 +144,14 @@ def cmd_relocate(args):
146
144
 
147
145
  def cmd_lint(args):
148
146
  from . import lint
147
+
149
148
  data = _data()
150
149
  mdir = store.memories_dir(data)
151
150
  memories = store.load_all(data)
152
151
  names = {m["name"] for m in memories if m["name"]}
153
- picked = [m for m in memories
154
- if not args.only or any(k in (m["id"], m["name"]) or k in m["rel"] for k in args.only)]
152
+ picked = [
153
+ m for m in memories if not args.only or any(k in (m["id"], m["name"]) or k in m["rel"] for k in args.only)
154
+ ]
155
155
  flagged, kinds = 0, {}
156
156
  for m in picked:
157
157
  try:
@@ -180,6 +180,7 @@ def cmd_lint(args):
180
180
 
181
181
  def cmd_ui(args):
182
182
  from . import ui, ui_server
183
+
183
184
  if args.fetch_model:
184
185
  return ui.fetch_model()
185
186
  ui_server.run(_data(), port=args.port, open_browser=not args.no_open, semantic=not args.keywords_only)
@@ -194,26 +195,31 @@ def cmd_sync(args):
194
195
 
195
196
  def cmd_init(args):
196
197
  from .install import init_data
198
+
197
199
  init_data(Path(args.data_dir).expanduser())
198
200
 
199
201
 
200
202
  def cmd_install(args):
201
203
  from .install import install
204
+
202
205
  install(dry_run=args.dry_run, claude=not args.no_claude, codex=not args.no_codex)
203
206
 
204
207
 
205
208
  def cmd_uninstall(args):
206
209
  from .install import uninstall
210
+
207
211
  uninstall(dry_run=args.dry_run)
208
212
 
209
213
 
210
214
  def cmd_import(args):
211
215
  from . import importers
216
+
212
217
  importers.run(args.source, _data(), dry_run=args.dry_run)
213
218
 
214
219
 
215
220
  def cmd_doctor(args):
216
221
  from .install import doctor
222
+
217
223
  doctor()
218
224
 
219
225
 
@@ -277,8 +283,9 @@ def main(argv=None):
277
283
  sp.add_argument("--port", type=int, default=8790)
278
284
  sp.add_argument("--no-open", action="store_true", help="print the link without opening a browser")
279
285
  sp.add_argument("--keywords-only", action="store_true", help="skip the embedding model")
280
- sp.add_argument("--fetch-model", action="store_true",
281
- help="download the local embedding model (about 625 MB) and exit")
286
+ sp.add_argument(
287
+ "--fetch-model", action="store_true", help="download the local embedding model (about 625 MB) and exit"
288
+ )
282
289
  sp.set_defaults(fn=cmd_ui)
283
290
 
284
291
  sp = sub.add_parser("sync", help="pull and push the data repository")
@@ -10,8 +10,7 @@ _GIT_ENV = {**os.environ, "GIT_OPTIONAL_LOCKS": "0", "GIT_TERMINAL_PROMPT": "0"}
10
10
 
11
11
  def _git(cwd, *args):
12
12
  try:
13
- r = subprocess.run(["git", "-C", cwd, *args], capture_output=True, text=True,
14
- timeout=3, env=_GIT_ENV)
13
+ r = subprocess.run(["git", "-C", cwd, *args], capture_output=True, text=True, timeout=3, env=_GIT_ENV)
15
14
  except (OSError, subprocess.SubprocessError):
16
15
  return ""
17
16
  return r.stdout.strip() if r.returncode == 0 else ""
@@ -18,7 +18,7 @@ def split(text):
18
18
  for i in range(1, len(lines)):
19
19
  if lines[i].strip() == FENCE:
20
20
  meta = parse("\n".join(lines[1:i]))
21
- body = "\n".join(lines[i + 1:]).lstrip("\n")
21
+ body = "\n".join(lines[i + 1 :]).lstrip("\n")
22
22
  return meta, body
23
23
  return {}, text
24
24
 
@@ -28,8 +28,7 @@ def join(meta, body):
28
28
 
29
29
 
30
30
  def parse(block):
31
- lines = [line.rstrip() for line in block.split("\n")
32
- if line.strip() and not line.lstrip().startswith("#")]
31
+ lines = [line.rstrip() for line in block.split("\n") if line.strip() and not line.lstrip().startswith("#")]
33
32
  if not lines:
34
33
  return {}
35
34
  value, _ = _parse_map(lines, 0, _indent(lines[0]))
@@ -43,8 +43,9 @@ class Lock:
43
43
 
44
44
 
45
45
  def git(data_dir, *args, timeout=30):
46
- return subprocess.run(["git", "-C", str(data_dir), *args], capture_output=True,
47
- text=True, timeout=timeout, env=_ENV)
46
+ return subprocess.run(
47
+ ["git", "-C", str(data_dir), *args], capture_output=True, text=True, timeout=timeout, env=_ENV
48
+ )
48
49
 
49
50
 
50
51
  def has_remote(data_dir):
@@ -112,8 +113,13 @@ def spawn_sync(data_dir, push):
112
113
  log_path = Path(data_dir) / ".cache" / "sync.out"
113
114
  try:
114
115
  with open(log_path, "a") as out:
115
- subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=out, stderr=out,
116
- start_new_session=True,
117
- env={**os.environ, "AGENT_MEMORY_DATA": str(data_dir)})
116
+ subprocess.Popen(
117
+ args,
118
+ stdin=subprocess.DEVNULL,
119
+ stdout=out,
120
+ stderr=out,
121
+ start_new_session=True,
122
+ env={**os.environ, "AGENT_MEMORY_DATA": str(data_dir)},
123
+ )
118
124
  except OSError as e:
119
125
  log(data_dir, f"spawn sync failed: {e}")
@@ -36,14 +36,17 @@ def session_start(payload, tool, data_dir):
36
36
  ctx = detect(payload.get("cwd"))
37
37
  sdir = capture.session_dir(data_dir, tool, sid)
38
38
  sdir.mkdir(parents=True, exist_ok=True)
39
- capture.write_session(sdir, {
40
- "tool": tool,
41
- "session_id": sid,
42
- "session_title": payload.get("session_title"),
43
- "model": payload.get("model"),
44
- "transcript_path": payload.get("transcript_path"),
45
- "ctx": ctx,
46
- })
39
+ capture.write_session(
40
+ sdir,
41
+ {
42
+ "tool": tool,
43
+ "session_id": sid,
44
+ "session_title": payload.get("session_title"),
45
+ "model": payload.get("model"),
46
+ "transcript_path": payload.get("transcript_path"),
47
+ "ctx": ctx,
48
+ },
49
+ )
47
50
 
48
51
  recovered = capture.recover_orphans(data_dir, keep=sdir)
49
52
  stamp = Path(data_dir) / ".cache" / "last-pull"
@@ -59,8 +62,7 @@ def session_start(payload, tool, data_dir):
59
62
  "staging": str(sdir),
60
63
  "mem": str(config.BIN),
61
64
  }
62
- return select.render(store.load_all(data_dir), ctx, paths,
63
- throwaway=workdir.is_throwaway(ctx.get("cwd")))
65
+ return select.render(store.load_all(data_dir), ctx, paths, throwaway=workdir.is_throwaway(ctx.get("cwd")))
64
66
 
65
67
 
66
68
  def flush(payload, tool, data_dir, final):
@@ -82,10 +84,17 @@ def main(event, tool):
82
84
  try:
83
85
  if event == "session-start":
84
86
  text = session_start(payload, tool, data_dir)
85
- print(json.dumps({"hookSpecificOutput": {
86
- "hookEventName": "SessionStart",
87
- "additionalContext": text,
88
- }}, ensure_ascii=False))
87
+ print(
88
+ json.dumps(
89
+ {
90
+ "hookSpecificOutput": {
91
+ "hookEventName": "SessionStart",
92
+ "additionalContext": text,
93
+ }
94
+ },
95
+ ensure_ascii=False,
96
+ )
97
+ )
89
98
  elif event in ("stop", "session-end"):
90
99
  flush(payload, tool, data_dir, final=event == "session-end")
91
100
  else:
@@ -32,14 +32,21 @@ def _git_source(cwd):
32
32
  def _record(*, name, typ, scope, description, body, created, source):
33
33
  when = parse_time(created) or now()
34
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}}
35
+ meta = {
36
+ "id": memory_id,
37
+ "name": slugify(name),
38
+ "type": typ,
39
+ "scope": scope,
40
+ "description": " ".join(description.split())[:300],
41
+ "created": when.isoformat(),
42
+ "source": {k: v for k, v in source.items() if v},
43
+ }
38
44
  return memory_id, meta, body
39
45
 
40
46
 
41
47
  # ---------------------------------------------------------------- Claude Code
42
48
 
49
+
43
50
  def _decode_project_dir(name):
44
51
  """Recover a folder from Claude's dash-encoded project dir name, if it still exists."""
45
52
  tokens = name.lstrip("-").split("-")
@@ -109,15 +116,24 @@ def claude_items():
109
116
  original_cwd, cwd = cwd, inferred
110
117
  created = legacy.get("modified") or meta.get("modified") or _mtime(path)
111
118
  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)
119
+ source = {
120
+ "tool": "claude-code",
121
+ "session_id": origin,
122
+ "session_name": title,
123
+ **_git_source(cwd),
124
+ "original_cwd": original_cwd,
125
+ "imported_from": "claude-code auto memory",
126
+ "original": "~/" + path.relative_to(HOME).as_posix(),
127
+ }
128
+ yield _record(
129
+ name=m["name"] or path.stem,
130
+ typ=m["type"],
131
+ scope="global" if m["type"] in ("user", "feedback") else "here",
132
+ description=m["description"],
133
+ body=body,
134
+ created=created,
135
+ source=source,
136
+ )
121
137
 
122
138
 
123
139
  def _mtime(path):
@@ -126,6 +142,7 @@ def _mtime(path):
126
142
 
127
143
  # ---------------------------------------------------------------- Codex
128
144
 
145
+
129
146
  def _sections(text):
130
147
  out, current = {}, None
131
148
  for line in text.splitlines():
@@ -158,9 +175,15 @@ def _codex_source(original, cwd=None, thread=None):
158
175
  inferred = workdir.infer(rollout, cwd, workdir.default_exclude())
159
176
  if inferred:
160
177
  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()}
178
+ return {
179
+ "tool": "codex",
180
+ "session_id": thread,
181
+ "session_name": title,
182
+ **_git_source(cwd),
183
+ "original_cwd": original_cwd,
184
+ "imported_from": "codex memories",
185
+ "original": "~/" + (CODEX_MEMORIES / original).relative_to(HOME).as_posix(),
186
+ }
164
187
 
165
188
 
166
189
  def codex_items():
@@ -171,17 +194,28 @@ def codex_items():
171
194
  profile = " ".join(line.strip() for line in sec.get("User Profile", []) if line.strip())
172
195
  if profile:
173
196
  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"))
197
+ yield _record(
198
+ name="user-profile-from-codex",
199
+ typ="user",
200
+ scope="global",
201
+ description="User profile as summarised by Codex: " + profile[:200],
202
+ body=profile + "\n",
203
+ created=created,
204
+ source=_codex_source("memory_summary.md#user-profile"),
205
+ )
178
206
  for heading in ("User preferences", "General Tips"):
179
207
  for n, text in enumerate(_bullets(sec.get(heading, []))):
180
208
  if ADHOC_TAG in text or text.startswith("Skills:"):
181
209
  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}"))
210
+ yield _record(
211
+ name=" ".join(text.split()[:6]),
212
+ typ="feedback",
213
+ scope="global",
214
+ description=text,
215
+ body=text + "\n",
216
+ created=created,
217
+ source=_codex_source(f"memory_summary.md#{slugify(heading)}-{n}"),
218
+ )
185
219
 
186
220
  handbook = CODEX_MEMORIES / "MEMORY.md"
187
221
  if handbook.exists():
@@ -189,27 +223,39 @@ def codex_items():
189
223
  for block in blocks:
190
224
  if not block.startswith("# Task Group: "):
191
225
  continue
192
- title = block.splitlines()[0][len("# Task Group: "):].strip()
226
+ title = block.splitlines()[0][len("# Task Group: ") :].strip()
193
227
  cwd = next(iter(re.findall(r"cwd=([^\s;,)]+)", block)), None)
194
228
  thread = next(iter(re.findall(r"thread_id=([0-9a-f-]{36})", block)), None)
195
229
  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))
230
+ yield _record(
231
+ name=title,
232
+ typ="project",
233
+ scope="here",
234
+ description=f"Codex task history: {title}",
235
+ body=block.rstrip() + "\n",
236
+ created=stamps[-1] if stamps else _mtime(handbook),
237
+ source=_codex_source(f"MEMORY.md#{slugify(title)}", cwd, thread),
238
+ )
200
239
 
201
240
  for path in sorted((CODEX_MEMORIES / "extensions" / "ad_hoc" / "notes").glob("*.md")):
202
241
  body = path.read_text("utf-8")
203
242
  _, raw = frontmatter.split(body)
204
243
  desc = normalize({}, raw, path.stem)["description"]
205
244
  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()))
245
+ yield _record(
246
+ name=name,
247
+ typ="reference",
248
+ scope="global",
249
+ description=desc,
250
+ body=raw,
251
+ created=_mtime(path),
252
+ source=_codex_source(path.relative_to(CODEX_MEMORIES).as_posix()),
253
+ )
209
254
 
210
255
 
211
256
  # ---------------------------------------------------------------- driver
212
257
 
258
+
213
259
  def run(which, data_dir, dry_run=False):
214
260
  items = list(claude_items() if which == "claude" else codex_items())
215
261
  seen = {m["source"].get("original") for m in load_all(data_dir)}