agent-memory-git 0.1.7__tar.gz → 0.1.9__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 (68) hide show
  1. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/PKG-INFO +3 -1
  2. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/README.md +2 -0
  3. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/pyproject.toml +1 -1
  4. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/pyproject.toml.orig +1 -1
  5. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/cli.py +53 -3
  6. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/config.py +6 -2
  7. agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/08/20260816-blog-publishing-k3qm.md +22 -0
  8. agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/09/20260906-push-personal-repos-w8re.md +22 -0
  9. agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/09/20260921-home-server-n2vb.md +18 -0
  10. agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/10/20261004-options-first-7hcx.md +20 -0
  11. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/demo.py +45 -17
  12. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/gitsync.py +43 -0
  13. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/hooks.py +20 -2
  14. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/install.py +104 -27
  15. agent_memory_git-0.1.9/src/agent_memory/profiles.py +309 -0
  16. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/select.py +2 -1
  17. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/ui_server.py +97 -10
  18. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/app.css +98 -2
  19. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/index.html +5 -0
  20. agent_memory_git-0.1.9/src/agent_memory/webui/js/avatar.js +58 -0
  21. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/icons.js +3 -0
  22. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/main.js +48 -2
  23. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/store.js +77 -3
  24. agent_memory_git-0.1.9/src/agent_memory/webui/js/views/profiles.js +85 -0
  25. agent_memory_git-0.1.9/src/agent_memory/webui/js/views/settings.js +138 -0
  26. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/shell.js +2 -1
  27. agent_memory_git-0.1.7/src/agent_memory/webui/js/views/settings.js +0 -84
  28. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/LICENSE +0 -0
  29. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/PROTOCOL.md +0 -0
  30. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/__init__.py +0 -0
  31. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/__main__.py +0 -0
  32. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/capture.py +0 -0
  33. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/context.py +0 -0
  34. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/personal}/2026/10/20261009-thesis-setup-fr6e.md +0 -0
  35. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/08/20260804-alex-profile-ujzd.md +0 -0
  36. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/08/20260804-reply-language-xdnc.md +0 -0
  37. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/08/20260812-ask-before-push-pf3d.md +0 -0
  38. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/08/20260820-tests-before-done-odzd.md +0 -0
  39. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/08/20260827-etl-orders-export-deadline-pft7.md +0 -0
  40. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/08/20260828-etl-dashboard-72ce.md +0 -0
  41. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/09/20260902-report-eta-is2j.md +0 -0
  42. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/09/20260908-shop-web-stripe-checkout-lgmx.md +0 -0
  43. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/09/20260910-shop-web-deploys-dn73.md +0 -0
  44. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/09/20260915-etl-parquet-partitioning-seh2.md +0 -0
  45. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/09/20260922-design-tokens-source-uvw7.md +0 -0
  46. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/10/20261002-shop-web-v3-freeze-5xtp.md +0 -0
  47. {agent_memory_git-0.1.7/src/agent_memory/demo/memories → agent_memory_git-0.1.9/src/agent_memory/demo/work}/2026/10/20261012-payments-runbook-4syw.md +0 -0
  48. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/embed_worker.py +0 -0
  49. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/frontmatter.py +0 -0
  50. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/importers.py +0 -0
  51. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/lint.py +0 -0
  52. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/sessions.py +0 -0
  53. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/store.py +0 -0
  54. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/ui.py +0 -0
  55. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/update.py +0 -0
  56. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/util.py +0 -0
  57. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/api.js +0 -0
  58. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/filters.js +0 -0
  59. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/format.js +0 -0
  60. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/h.js +0 -0
  61. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/markdown.js +0 -0
  62. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/search.js +0 -0
  63. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/insights.js +0 -0
  64. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/list.js +0 -0
  65. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/memory.js +0 -0
  66. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/overlays.js +0 -0
  67. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/ui.js +0 -0
  68. {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/workdir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-memory-git
3
- Version: 0.1.7
3
+ Version: 0.1.9
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
@@ -58,6 +58,8 @@ Description-Content-Type: text/markdown
58
58
  - **Memory in git.** One Markdown file per memory in a git repository you own, with where it came
59
59
  from recorded by the hooks. Every change is a commit you can review or revert; add a private
60
60
  remote to sync machines, or keep it all on one. Here is [an example](https://github.com/Ki-Seki/agent-memory-data-demo).
61
+ [Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep work and personal
62
+ memories in repositories of their own.
61
63
  - **Nothing to run.** Two hooks and git. No service, no account; search by meaning runs on your CPU.
62
64
  - **A page to keep it tidy.** `mem ui` searches by words and by meaning, edits with undo, and shows
63
65
  what changed week by week.
@@ -37,6 +37,8 @@
37
37
  - **Memory in git.** One Markdown file per memory in a git repository you own, with where it came
38
38
  from recorded by the hooks. Every change is a commit you can review or revert; add a private
39
39
  remote to sync machines, or keep it all on one. Here is [an example](https://github.com/Ki-Seki/agent-memory-data-demo).
40
+ [Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep work and personal
41
+ memories in repositories of their own.
40
42
  - **Nothing to run.** Two hooks and git. No service, no account; search by meaning runs on your CPU.
41
43
  - **A page to keep it tidy.** `mem ui` searches by words and by meaning, edits with undo, and shows
42
44
  what changed week by week.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.7"
3
+ version = "0.1.9"
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.7"
3
+ version = "0.1.9"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -8,7 +8,7 @@ import signal
8
8
  import sys
9
9
  from pathlib import Path
10
10
 
11
- from . import capture, config, frontmatter, gitsync, hooks, select, store
11
+ from . import capture, config, frontmatter, gitsync, hooks, profiles, select, store
12
12
  from .context import detect
13
13
  from .util import SCOPES, TYPES, atomic_write, slugify
14
14
 
@@ -79,7 +79,7 @@ def cmd_index(args):
79
79
  data = _data()
80
80
  ctx = detect(args.cwd)
81
81
  paths = {"memories": str(store.memories_dir(data)), "staging": "(per session)", "mem": str(config.BIN)}
82
- text = select.render(store.load_all(data), ctx, paths)
82
+ text = select.render(store.load_all(data), ctx, paths, profile=profiles.active_name())
83
83
  print(text, end="")
84
84
  print(f"\n[{len(text)} characters]", file=sys.stderr)
85
85
 
@@ -195,7 +195,7 @@ def cmd_ui(args):
195
195
  )
196
196
  os.execv(sys.executable, [sys.executable, "-m", "agent_memory", *again])
197
197
  if os.environ.get(demo.ENV):
198
- print("Demo: made-up memories in a temporary repository; nothing you change is kept.", flush=True)
198
+ print("Demo: made-up memories in temporary repositories; nothing you change is kept.", flush=True)
199
199
  atexit.register(shutil.rmtree, os.environ[demo.ENV], ignore_errors=True)
200
200
  # closing the terminal (SIGHUP) or `kill` (SIGTERM) would end the process without that clean-up
201
201
  for name in ("SIGHUP", "SIGTERM"):
@@ -241,6 +241,43 @@ def cmd_doctor(args):
241
241
  return doctor()
242
242
 
243
243
 
244
+ def _access_note():
245
+ from .install import refresh_access
246
+
247
+ tools = refresh_access()
248
+ if tools:
249
+ print(f"updated {' and '.join(tools)} so agents can reach every profile's folder")
250
+
251
+
252
+ def cmd_profile(args):
253
+ if profiles.locked():
254
+ sys.exit("AGENT_MEMORY_DATA picks the data repository here, so profiles do not apply")
255
+ try:
256
+ if args.action == "use":
257
+ pid = profiles.use(args.profile)
258
+ print(f"new sessions now use {profiles.listing()[0][pid]['name']}")
259
+ elif args.action == "add":
260
+ pid, note = profiles.add(args.folder, remote=args.remote, switch=not args.keep)
261
+ found, active = profiles.listing()
262
+ print(note)
263
+ print(f"added {found[pid]['name']}" + ("; new sessions now use it" if active == pid else ""))
264
+ _access_note()
265
+ elif args.action == "remove":
266
+ found, _ = profiles.listing()
267
+ pid = profiles.find(args.profile, found)
268
+ active = profiles.remove(pid)
269
+ print(f"removed {found[pid]['name']}; {profiles.short(found[pid]['data_dir'])} is left as it is")
270
+ print(f"new sessions use {profiles.listing()[0][active]['name']}")
271
+ _access_note()
272
+ else:
273
+ for p in profiles.summary()["profiles"]:
274
+ where = p["remote"] or "this machine only"
275
+ mark = "*" if p["active"] else " "
276
+ print(f"{mark} {p['name']:<18} {p['dataDir']} ({p['memories']} memories, {where})")
277
+ except profiles.ProfileError as e:
278
+ sys.exit(str(e))
279
+
280
+
244
281
  def cmd_hook(args):
245
282
  return hooks.main(args.event, args.tool)
246
283
 
@@ -336,6 +373,19 @@ def main(argv=None):
336
373
  sp = sub.add_parser("doctor", help="check the installation")
337
374
  sp.set_defaults(fn=cmd_doctor)
338
375
 
376
+ sp = sub.add_parser("profile", help="list, switch, add or remove profiles: separate memory repositories")
377
+ psub = sp.add_subparsers(dest="action")
378
+ psub.add_parser("list", help="list profiles; * marks the one new sessions use")
379
+ pp = psub.add_parser("use", help="make new sessions (and mem ui) use this profile")
380
+ pp.add_argument("profile", help="its name, such as quiet-harbor")
381
+ pp = psub.add_parser("add", help="add a profile for a new, empty or existing memory repository; it gets a name")
382
+ pp.add_argument("folder", help="its folder; a new or empty one becomes a repository, or a clone of --remote")
383
+ pp.add_argument("--remote", help="a private git remote of its own (optional)")
384
+ pp.add_argument("--keep", action="store_true", help="add it without switching to it")
385
+ pp = psub.add_parser("remove", help="forget a profile; its folder and remote are left as they are")
386
+ pp.add_argument("profile", help="its name, such as quiet-harbor")
387
+ sp.set_defaults(fn=cmd_profile)
388
+
339
389
  sp = sub.add_parser("hook", help=argparse.SUPPRESS)
340
390
  sp.add_argument("event", choices=["session-start", "stop", "session-end"])
341
391
  sp.add_argument("--tool", required=True, choices=["claude-code", "codex"])
@@ -52,6 +52,10 @@ def save(cfg):
52
52
 
53
53
 
54
54
  def data_dir():
55
- """The data repository, or None when not configured."""
56
- value = os.environ.get("AGENT_MEMORY_DATA") or load().get("data_dir")
55
+ """The data repository: AGENT_MEMORY_DATA when set, else the active profile's folder; None when not configured."""
56
+ value = os.environ.get("AGENT_MEMORY_DATA")
57
+ if not value:
58
+ cfg = load()
59
+ profiles = cfg.get("profiles") if isinstance(cfg.get("profiles"), dict) else {}
60
+ value = (profiles.get(cfg.get("profile")) or {}).get("data_dir") or cfg.get("data_dir")
57
61
  return Path(value).expanduser() if value else None
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: 20260816-blog-publishing-k3qm
3
+ name: blog-publishing
4
+ type: project
5
+ scope: here
6
+ description: "blog: posts are Markdown in content/posts and a push to main publishes them; drafts stay in content/drafts"
7
+ created: 2026-08-16T20:40:00+08:00
8
+ source:
9
+ tool: codex
10
+ session_id: 58653a77-5fdc-4a8c-91e3-2a91b3a7875d
11
+ session_name: blog-rss-feed
12
+ host: alex-laptop
13
+ cwd: /Users/alex/code/blog
14
+ git_root: /Users/alex/code/blog
15
+ git_remote: git.example.com/blog
16
+ git_branch: main
17
+ ---
18
+ The blog is a static site: posts are Markdown files in `content/posts`, and every push to `main` builds and publishes it. Unfinished posts go in `content/drafts`, which is never built.
19
+
20
+ **Why:** there is no review step, so anything on `main` is live within a minute.
21
+
22
+ **How to apply:** start new posts in `content/drafts`, and move one to `content/posts` only when Alex says to publish it.
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: 20260906-push-personal-repos-w8re
3
+ name: push-personal-repos
4
+ type: feedback
5
+ scope: global
6
+ description: In personal repositories such as the blog and homelab, commit and push without asking first
7
+ created: 2026-09-06T11:15:00+08:00
8
+ source:
9
+ tool: claude-code
10
+ session_id: 7bbe96a3-54ec-4718-b2b4-2b0158b7d571
11
+ session_name: blog-typo-fixes
12
+ host: alex-laptop
13
+ cwd: /Users/alex/code/blog
14
+ git_root: /Users/alex/code/blog
15
+ git_remote: git.example.com/blog
16
+ git_branch: main
17
+ ---
18
+ In Alex's personal repositories, such as the blog and homelab, commit and push directly; there is no need to ask before each push.
19
+
20
+ **Why:** they are repositories only Alex works in, with no shared branches, and asking every time slowed down small fixes.
21
+
22
+ **How to apply:** still keep each commit to one change, and say afterwards what was pushed.
@@ -0,0 +1,18 @@
1
+ ---
2
+ id: 20260921-home-server-n2vb
3
+ name: home-server
4
+ type: reference
5
+ scope: global
6
+ description: "Home server nas.home.arpa: Jellyfin on port 8096, nightly backups of /srv to the USB drive at 03:00"
7
+ created: 2026-09-21T16:30:00+08:00
8
+ source:
9
+ tool: codex
10
+ session_id: de0fd804-e67d-4793-9580-dd285f8f7eda
11
+ session_name: homelab-backups
12
+ host: alex-laptop
13
+ cwd: /Users/alex/code/homelab
14
+ git_root: /Users/alex/code/homelab
15
+ git_remote: git.example.com/homelab
16
+ git_branch: main
17
+ ---
18
+ The home server is `nas.home.arpa` on the home network. Jellyfin runs on port 8096, configured from `jellyfin/` in the homelab repository. `/srv` is backed up every night at 03:00 to the USB drive mounted at `/mnt/backup`.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: 20261004-options-first-7hcx
3
+ name: options-first
4
+ type: user
5
+ scope: global
6
+ description: 个人项目里先给两三个方案和各自的取舍,Alex 选定后再动手写
7
+ created: 2026-10-04T15:05:00+08:00
8
+ source:
9
+ tool: claude-code
10
+ session_id: f190571a-9a40-42f5-be8d-2a49882c84b4
11
+ session_name: 博客评论功能
12
+ host: alex-laptop
13
+ cwd: /Users/alex/code/blog
14
+ git_root: /Users/alex/code/blog
15
+ git_remote: git.example.com/blog
16
+ git_branch: main
17
+ ---
18
+ 做个人项目时,Alex 喜欢先看两三个可选方案和各自的取舍,自己挑一个,再开始写代码。
19
+
20
+ 个人项目主要是为了学东西,选方案的过程本身就是目的;工作项目里则直接按团队惯例做。
@@ -1,8 +1,9 @@
1
1
  """Made-up memories to try agent-memory with, without installing it or touching your own.
2
2
 
3
- `mem ui --demo` copies them into a temporary data repository whose history has one commit per
4
- memory, the way the hooks write them, and opens the page on it with its own config folder. The
5
- project site's live demo is built from the same memories (scripts/site.py).
3
+ There are two profiles of them, one for work and one for home. `mem ui --demo` copies each into a temporary data
4
+ repository whose history has one commit per memory, the way the hooks write them, and opens the page on
5
+ them with a config folder of its own. The project site's live demo is built from the same memories
6
+ (scripts/site.py).
6
7
  """
7
8
 
8
9
  import datetime as _dt
@@ -16,11 +17,20 @@ from pathlib import Path
16
17
 
17
18
  from . import config, frontmatter, gitsync, install, store
18
19
 
19
- MEMORIES = Path(__file__).with_name("demo") / "memories"
20
+ DEMO = Path(__file__).with_name("demo")
21
+ # the demo's profiles, the active one first, named as profiles.new_name would; each one's memories are in demo/<id>
22
+ PROFILES = {
23
+ "work": {"name": "Quiet Harbor", "folder": "agent-memory-data"},
24
+ "personal": {"name": "Sunny Orchard", "folder": "agent-memory-personal"},
25
+ }
20
26
  ENV = "AGENT_MEMORY_DEMO" # set to the demo's temporary folder while a demo runs
21
27
  DATES = ("created", "updated")
22
28
 
23
29
 
30
+ def memories(profile):
31
+ return DEMO / profile
32
+
33
+
24
34
  def _when(value):
25
35
  try:
26
36
  return _dt.datetime.fromisoformat(str(value))
@@ -35,23 +45,31 @@ def _shift(meta, days):
35
45
  meta[key] = (when + _dt.timedelta(days=days)).isoformat()
36
46
 
37
47
 
38
- def make(dest, today=None):
39
- """A data repository at dest holding the demo memories.
48
+ def _newest():
49
+ """When the newest demo memory, in any profile, was written or changed."""
50
+ stamps = []
51
+ for path in DEMO.rglob("*.md"):
52
+ meta, _ = frontmatter.split(path.read_text("utf-8"))
53
+ stamps += [_when(meta.get(k)) for k in DATES if _when(meta.get(k))]
54
+ return max(stamps)
55
+
56
+
57
+ def make(dest, today=None, profile="work"):
58
+ """A data repository at dest holding one profile's demo memories.
40
59
 
41
60
  Each memory is committed on its own at the time it was created (and again when it was changed),
42
61
  with the subject and trailers the Stop hook writes, so Insights has a history to show. With `today`, every date
43
- moves by the same number of days so the newest memory is a day old.
62
+ moves by the same number of days so the newest memory of all the profiles is a day old.
44
63
  """
45
64
  dest = Path(dest)
46
65
  mdir = store.memories_dir(dest)
47
- shutil.copytree(MEMORIES, mdir)
66
+ shutil.copytree(memories(profile), mdir)
48
67
  (dest / ".gitignore").write_text("staging/\n.cache/\n", "utf-8")
49
68
  (dest / "README.md").write_text(install.DATA_README, "utf-8")
50
69
  files = sorted(mdir.rglob("*.md"))
51
70
  docs = {path: frontmatter.split(path.read_text("utf-8")) for path in files}
52
71
  if today:
53
- newest = max(_when(meta.get(k)) for meta, _ in docs.values() for k in DATES if _when(meta.get(k)))
54
- days = (today - newest.date()).days - 1
72
+ days = (today - _newest().date()).days - 1
55
73
  for meta, _ in docs.values():
56
74
  _shift(meta, days)
57
75
 
@@ -93,20 +111,30 @@ def make(dest, today=None):
93
111
 
94
112
 
95
113
  def prepare():
96
- """A temporary demo: data repository and config folder, named in the environment for `mem ui`."""
114
+ """A temporary demo: a data repository per profile and a config folder, named in the environment for `mem ui`.
115
+
116
+ Profiles work in it as they do in yours, except that new ones have to stay in its temporary folder.
117
+ """
97
118
  root = Path(tempfile.mkdtemp(prefix="agent-memory-demo-"))
98
- data, cfg_dir = root / "data", root / "config"
99
- make(data, today=_dt.date.today())
119
+ cfg_dir = root / "config"
120
+ profiles = {}
121
+ for pid, p in PROFILES.items():
122
+ data = root / p["folder"]
123
+ make(data, today=_dt.date.today(), profile=pid)
124
+ profiles[pid] = {"name": p["name"], "data_dir": str(data)}
100
125
  cfg_dir.mkdir()
101
126
  keep = {k: v for k, v in config.load().items() if k == "embed_model_dir"} # where the model already is
102
- (cfg_dir / "config.json").write_text(json.dumps({"data_dir": str(data), **keep}), "utf-8")
103
- os.environ.update({ENV: str(root), "AGENT_MEMORY_DATA": str(data), "AGENT_MEMORY_CONFIG_DIR": str(cfg_dir)})
127
+ active = next(iter(profiles))
128
+ cfg = {"data_dir": profiles[active]["data_dir"], "profiles": profiles, "profile": active, **keep}
129
+ (cfg_dir / "config.json").write_text(json.dumps(cfg, ensure_ascii=False), "utf-8")
130
+ os.environ.pop("AGENT_MEMORY_DATA", None) # the profiles pick the folder
131
+ os.environ.update({ENV: str(root), "AGENT_MEMORY_CONFIG_DIR": str(cfg_dir)})
104
132
  return root
105
133
 
106
134
 
107
135
  def fingerprint():
108
136
  """A hash of the demo memories, so the site's saved demo data can tell when they change."""
109
137
  h = hashlib.sha256()
110
- for path in sorted(MEMORIES.rglob("*.md")):
111
- h.update(str(path.relative_to(MEMORIES)).encode() + b"\0" + path.read_bytes() + b"\0")
138
+ for path in sorted(DEMO.rglob("*.md")):
139
+ h.update(str(path.relative_to(DEMO)).encode() + b"\0" + path.read_bytes() + b"\0")
112
140
  return h.hexdigest()[:16]
@@ -2,6 +2,7 @@
2
2
 
3
3
  import fcntl
4
4
  import os
5
+ import re
5
6
  import subprocess
6
7
  import time
7
8
  from pathlib import Path
@@ -10,6 +11,9 @@ from .config import mem_command
10
11
  from .util import log
11
12
 
12
13
  _ENV = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
14
+ # what a remote may look like: a URL, scp-style user@host:path, or a folder; never something git reads as an option
15
+ REMOTE_URL = re.compile(r"^(?:(?:https?|ssh|git|file)://\S+|[\w.-]+@[\w.-]+:\S+|[~/]\S*)$")
16
+ BAD_REMOTE = "That does not look like a git remote: use https://…, git@host:owner/repo.git, or a folder."
13
17
 
14
18
 
15
19
  class Lock:
@@ -123,3 +127,42 @@ def spawn_sync(data_dir, push):
123
127
  )
124
128
  except OSError as e:
125
129
  log(data_dir, f"spawn sync failed: {e}")
130
+
131
+
132
+ def remote_url(url):
133
+ """The remote as git should get it, or ValueError when it does not look like one."""
134
+ url = str(url).strip()
135
+ if not REMOTE_URL.match(url):
136
+ raise ValueError(BAD_REMOTE)
137
+ return os.path.expanduser(url) if url.startswith("~") else url # git takes a folder as given
138
+
139
+
140
+ def connect(data_dir, url):
141
+ """Make `url` the data repository's origin: (ok, message).
142
+
143
+ A remote that already has this branch is pulled from first; an empty one gets this branch pushed to it,
144
+ so the hooks' pull and push work from then on. ValueError for something that is not a remote.
145
+ """
146
+ url = remote_url(url)
147
+ verb = "set-url" if git(data_dir, "remote", "get-url", "origin").returncode == 0 else "add"
148
+ r = git(data_dir, "remote", verb, "origin", url)
149
+ if r.returncode != 0:
150
+ raise ValueError(f"git remote {verb} failed: {(r.stderr or r.stdout).strip()}")
151
+ branch = git(data_dir, "branch", "--show-current").stdout.strip() or "main"
152
+ fetch = git(data_dir, "fetch", "-q", "origin", timeout=60)
153
+ if fetch.returncode != 0:
154
+ why = fetch.stderr.strip().splitlines()[-1] if fetch.stderr.strip() else "no answer"
155
+ return False, f"Saved, but the remote could not be reached: {why}"
156
+ if git(data_dir, "rev-parse", "--verify", "-q", f"origin/{branch}").returncode == 0:
157
+ git(data_dir, "branch", f"--set-upstream-to=origin/{branch}")
158
+ ok = sync(data_dir)
159
+ return ok, "Connected and synced." if ok else "Connected, but syncing failed; see .cache/agent-memory.log."
160
+ push = git(data_dir, "push", "-q", "-u", "origin", branch, timeout=60)
161
+ if push.returncode != 0:
162
+ return False, f"Saved, but the push failed: {push.stderr.strip()}"
163
+ return True, "Connected; this repository is pushed to it."
164
+
165
+
166
+ def disconnect(data_dir):
167
+ if has_remote(data_dir):
168
+ git(data_dir, "remote", "remove", "origin")
@@ -12,7 +12,7 @@ import sys
12
12
  import time
13
13
  from pathlib import Path
14
14
 
15
- from . import capture, config, gitsync, select, store, workdir
15
+ from . import capture, config, gitsync, profiles, select, store, workdir
16
16
  from .context import detect
17
17
  from .util import log, log_exc
18
18
 
@@ -62,7 +62,13 @@ def session_start(payload, tool, data_dir):
62
62
  "staging": str(sdir),
63
63
  "mem": str(config.BIN),
64
64
  }
65
- return select.render(store.load_all(data_dir), ctx, paths, throwaway=workdir.is_throwaway(ctx.get("cwd")))
65
+ return select.render(
66
+ store.load_all(data_dir),
67
+ ctx,
68
+ paths,
69
+ throwaway=workdir.is_throwaway(ctx.get("cwd")),
70
+ profile=profiles.active_name(),
71
+ )
66
72
 
67
73
 
68
74
  def flush(payload, tool, data_dir, final):
@@ -76,9 +82,21 @@ def flush(payload, tool, data_dir, final):
76
82
  gitsync.spawn_sync(data_dir, push=True)
77
83
 
78
84
 
85
+ def _home(payload, tool, active):
86
+ """The data repository holding this session's staging folder: the profile that was active when the
87
+ session started, which need not be the active one now."""
88
+ sid = payload.get("session_id")
89
+ for data_dir in profiles.dirs() if sid else []:
90
+ if capture.session_dir(data_dir, tool, str(sid)).is_dir():
91
+ return data_dir
92
+ return active
93
+
94
+
79
95
  def main(event, tool):
80
96
  payload = _payload()
81
97
  data_dir = config.data_dir()
98
+ if data_dir and event != "session-start":
99
+ data_dir = _home(payload, tool, data_dir)
82
100
  if not data_dir or not (data_dir / ".git").exists():
83
101
  return 0
84
102
  try: