agent-memory-git 0.1.0__tar.gz → 0.1.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.
Files changed (29) hide show
  1. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/PKG-INFO +18 -12
  2. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/README.md +17 -11
  3. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/pyproject.toml +1 -1
  4. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/pyproject.toml.orig +1 -1
  5. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/capture.py +4 -4
  6. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/cli.py +17 -10
  7. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/config.py +1 -9
  8. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/context.py +1 -2
  9. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/frontmatter.py +2 -3
  10. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/gitsync.py +11 -5
  11. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/hooks.py +23 -14
  12. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/importers.py +76 -30
  13. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/install.py +90 -42
  14. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/lint.py +9 -3
  15. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/select.py +1 -2
  16. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/sessions.py +1 -2
  17. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/ui.py +109 -53
  18. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/ui_server.py +72 -23
  19. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/workdir.py +10 -7
  20. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/LICENSE +0 -0
  21. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/PROTOCOL.md +0 -0
  22. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/__init__.py +0 -0
  23. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/__main__.py +0 -0
  24. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/embed_worker.py +0 -0
  25. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/store.py +0 -0
  26. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/util.py +0 -0
  27. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/webui/app.css +0 -0
  28. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/webui/app.js +0 -0
  29. {agent_memory_git-0.1.0 → agent_memory_git-0.1.2}/src/agent_memory/webui/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-memory-git
3
- Version: 0.1.0
3
+ Version: 0.1.2
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
@@ -30,25 +30,25 @@ you gave Codex is there the next time Claude Code starts, on any machine that pu
30
30
  repository. This repository holds only the code; the memories live in a separate data
31
31
  repository.
32
32
 
33
- ![mem ui listing memories, with one open on the right](docs/mem-ui.png)
33
+ ![mem ui listing memories, with one open on the right](https://raw.githubusercontent.com/Ki-Seki/agent-memory/main/docs/mem-ui.png)
34
34
 
35
35
  <sub>`mem ui` on the [demo data repository](https://github.com/Ki-Seki/agent-memory-data-demo).</sub>
36
36
 
37
37
  ## Try it without installing
38
38
 
39
39
  ```bash
40
- git clone https://github.com/Ki-Seki/agent-memory
41
40
  git clone https://github.com/Ki-Seki/agent-memory-data-demo
42
- cd agent-memory
43
- AGENT_MEMORY_DATA=../agent-memory-data-demo bin/mem ui --keywords-only
41
+ AGENT_MEMORY_DATA=agent-memory-data-demo uvx --from "agent-memory-git[ui]" mem ui --keywords-only
44
42
  ```
45
43
 
46
- `AGENT_MEMORY_DATA` points `mem` at the demo repository, so nothing is installed and no hooks
47
- change. The page needs `numpy`. `--keywords-only` skips the optional embedding model; leave it
48
- out after `mem ui --fetch-model` to also search by meaning.
44
+ `uvx` runs `mem` from a throwaway environment and `AGENT_MEMORY_DATA` points it at the demo
45
+ repository, so nothing is installed and no hooks change. `--keywords-only` skips the optional
46
+ embedding model; leave it out after `mem ui --fetch-model` to also search by meaning.
49
47
 
50
48
  ## How it works
51
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
+
52
52
  - **One file per memory.** Frontmatter carries `type` (user / feedback / project / reference),
53
53
  `scope` (global / here), a one-line `description`, and provenance under `source`: tool, session
54
54
  id and name, model, host, folder, git root, remote, branch and commit. Files live in
@@ -99,7 +99,7 @@ protocol copies; `mem doctor` says when they are out of date.
99
99
  | `~/.codex/hooks.json` | the same three hooks |
100
100
  | `~/.codex/config.toml` | `memories.generate_memories = false`, `memories.use_memories = false`, data repository added to `sandbox_workspace_write.writable_roots` |
101
101
  | `~/.codex/AGENTS.md` | a marked block with the protocol |
102
- | `~/.local/bin/mem` | symlink to `mem`, only if that file does not exist yet (`uv tool install` already puts one there) |
102
+ | `~/.zshrc` or `~/.bashrc` | a marked block putting `~/.local/bin`, where uv installs `mem`, on PATH, only if a new login shell lacks it |
103
103
 
104
104
  Codex asks you to trust new hooks once: open `/hooks` in Codex and enable the three
105
105
  agent-memory hooks.
@@ -159,11 +159,17 @@ prints, so other web pages cannot read or change memories.
159
159
  uv sync --all-extras # dev tools and numpy
160
160
  uv run pytest
161
161
  uv run ruff check .
162
+ uv run ruff format .
162
163
  ```
163
164
 
164
- From a checkout, `bin/mem` (or `uv run mem`) runs the code in place; `mem install` from there
165
- points the hooks at that checkout, so edits take effect in the next session.
166
- `uv tool install --editable .` does the same with `mem` on PATH.
165
+ `uv run mem ...` runs the working copy without touching the installed one. To have every
166
+ session use the working copy, install it in place of the release and rewire the hooks; go back
167
+ the same way:
168
+
169
+ ```bash
170
+ uv tool install --force --editable ".[ui]" && mem install # the checkout, live
171
+ uv tool install --force "agent-memory-git[ui]" && mem install # the release again
172
+ ```
167
173
 
168
174
  To release, run `uv run scripts/release.py prepare patch` (or `minor`, `major`) to open a
169
175
  version PR. After it is merged, `uv run scripts/release.py publish <PR_NUMBER>` creates the GitHub
@@ -10,25 +10,25 @@ you gave Codex is there the next time Claude Code starts, on any machine that pu
10
10
  repository. This repository holds only the code; the memories live in a separate data
11
11
  repository.
12
12
 
13
- ![mem ui listing memories, with one open on the right](docs/mem-ui.png)
13
+ ![mem ui listing memories, with one open on the right](https://raw.githubusercontent.com/Ki-Seki/agent-memory/main/docs/mem-ui.png)
14
14
 
15
15
  <sub>`mem ui` on the [demo data repository](https://github.com/Ki-Seki/agent-memory-data-demo).</sub>
16
16
 
17
17
  ## Try it without installing
18
18
 
19
19
  ```bash
20
- git clone https://github.com/Ki-Seki/agent-memory
21
20
  git clone https://github.com/Ki-Seki/agent-memory-data-demo
22
- cd agent-memory
23
- AGENT_MEMORY_DATA=../agent-memory-data-demo bin/mem ui --keywords-only
21
+ AGENT_MEMORY_DATA=agent-memory-data-demo uvx --from "agent-memory-git[ui]" mem ui --keywords-only
24
22
  ```
25
23
 
26
- `AGENT_MEMORY_DATA` points `mem` at the demo repository, so nothing is installed and no hooks
27
- change. The page needs `numpy`. `--keywords-only` skips the optional embedding model; leave it
28
- out after `mem ui --fetch-model` to also search by meaning.
24
+ `uvx` runs `mem` from a throwaway environment and `AGENT_MEMORY_DATA` points it at the demo
25
+ repository, so nothing is installed and no hooks change. `--keywords-only` skips the optional
26
+ embedding model; leave it out after `mem ui --fetch-model` to also search by meaning.
29
27
 
30
28
  ## How it works
31
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
+
32
32
  - **One file per memory.** Frontmatter carries `type` (user / feedback / project / reference),
33
33
  `scope` (global / here), a one-line `description`, and provenance under `source`: tool, session
34
34
  id and name, model, host, folder, git root, remote, branch and commit. Files live in
@@ -79,7 +79,7 @@ protocol copies; `mem doctor` says when they are out of date.
79
79
  | `~/.codex/hooks.json` | the same three hooks |
80
80
  | `~/.codex/config.toml` | `memories.generate_memories = false`, `memories.use_memories = false`, data repository added to `sandbox_workspace_write.writable_roots` |
81
81
  | `~/.codex/AGENTS.md` | a marked block with the protocol |
82
- | `~/.local/bin/mem` | symlink to `mem`, only if that file does not exist yet (`uv tool install` already puts one there) |
82
+ | `~/.zshrc` or `~/.bashrc` | a marked block putting `~/.local/bin`, where uv installs `mem`, on PATH, only if a new login shell lacks it |
83
83
 
84
84
  Codex asks you to trust new hooks once: open `/hooks` in Codex and enable the three
85
85
  agent-memory hooks.
@@ -139,11 +139,17 @@ prints, so other web pages cannot read or change memories.
139
139
  uv sync --all-extras # dev tools and numpy
140
140
  uv run pytest
141
141
  uv run ruff check .
142
+ uv run ruff format .
142
143
  ```
143
144
 
144
- From a checkout, `bin/mem` (or `uv run mem`) runs the code in place; `mem install` from there
145
- points the hooks at that checkout, so edits take effect in the next session.
146
- `uv tool install --editable .` does the same with `mem` on PATH.
145
+ `uv run mem ...` runs the working copy without touching the installed one. To have every
146
+ session use the working copy, install it in place of the release and rewire the hooks; go back
147
+ the same way:
148
+
149
+ ```bash
150
+ uv tool install --force --editable ".[ui]" && mem install # the checkout, live
151
+ uv tool install --force "agent-memory-git[ui]" && mem install # the release again
152
+ ```
147
153
 
148
154
  To release, run `uv run scripts/release.py prepare patch` (or `minor`, `major`) to open a
149
155
  version PR. After it is merged, `uv run scripts/release.py publish <PR_NUMBER>` creates the GitHub
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.0"
3
+ version = "0.1.2"
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.0"
3
+ version = "0.1.2"
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")
@@ -7,11 +7,7 @@ from pathlib import Path
7
7
  PACKAGE_DIR = Path(__file__).resolve().parent
8
8
  PROTOCOL = PACKAGE_DIR / "PROTOCOL.md"
9
9
 
10
- # Running from a source checkout (bin/mem, `uv run`, or an editable install) rather than an
11
- # installed package: hooks then call the checkout's bin/mem, so edits take effect at once.
12
- _ROOT = PACKAGE_DIR.parent.parent
13
- CHECKOUT = _ROOT if (_ROOT / "pyproject.toml").is_file() and (_ROOT / "bin" / "mem").is_file() else None
14
- CODE_DIR = CHECKOUT or PACKAGE_DIR
10
+ CODE_DIR = PACKAGE_DIR
15
11
 
16
12
  CONFIG_DIR = Path(os.environ.get("AGENT_MEMORY_CONFIG_DIR", "~/.config/agent-memory")).expanduser()
17
13
  CONFIG_FILE = CONFIG_DIR / "config.json"
@@ -20,14 +16,10 @@ PROTOCOL_COPY = CONFIG_DIR / "PROTOCOL.md" # what Claude Code imports; `mem ins
20
16
 
21
17
  def mem_command():
22
18
  """The argv that runs `mem` with this interpreter, for hooks and background sync."""
23
- if CHECKOUT:
24
- return [sys.executable, str(CHECKOUT / "bin" / "mem")]
25
19
  return [sys.executable, "-m", "agent_memory"]
26
20
 
27
21
 
28
22
  def _mem_path():
29
- if CHECKOUT:
30
- return CHECKOUT / "bin" / "mem"
31
23
  found = shutil.which("mem")
32
24
  return Path(found) if found else Path(sys.executable).with_name("mem")
33
25
 
@@ -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)}