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.
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/PKG-INFO +3 -1
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/README.md +2 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/pyproject.toml +1 -1
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/pyproject.toml.orig +1 -1
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/cli.py +53 -3
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/config.py +6 -2
- agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/08/20260816-blog-publishing-k3qm.md +22 -0
- agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/09/20260906-push-personal-repos-w8re.md +22 -0
- agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/09/20260921-home-server-n2vb.md +18 -0
- agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/10/20261004-options-first-7hcx.md +20 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/demo.py +45 -17
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/gitsync.py +43 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/hooks.py +20 -2
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/install.py +104 -27
- agent_memory_git-0.1.9/src/agent_memory/profiles.py +309 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/select.py +2 -1
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/ui_server.py +97 -10
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/app.css +98 -2
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/index.html +5 -0
- agent_memory_git-0.1.9/src/agent_memory/webui/js/avatar.js +58 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/icons.js +3 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/main.js +48 -2
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/store.js +77 -3
- agent_memory_git-0.1.9/src/agent_memory/webui/js/views/profiles.js +85 -0
- agent_memory_git-0.1.9/src/agent_memory/webui/js/views/settings.js +138 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/shell.js +2 -1
- agent_memory_git-0.1.7/src/agent_memory/webui/js/views/settings.js +0 -84
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/LICENSE +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/PROTOCOL.md +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/__init__.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/__main__.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/capture.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/context.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/embed_worker.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/frontmatter.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/importers.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/lint.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/sessions.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/store.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/ui.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/update.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/util.py +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/api.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/filters.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/format.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/h.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/markdown.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/search.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/insights.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/list.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/memory.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/overlays.js +0 -0
- {agent_memory_git-0.1.7 → agent_memory_git-0.1.9}/src/agent_memory/webui/js/views/ui.js +0 -0
- {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.
|
|
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.
|
|
@@ -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
|
|
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,
|
|
56
|
-
value = os.environ.get("AGENT_MEMORY_DATA")
|
|
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
|
agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/08/20260816-blog-publishing-k3qm.md
ADDED
|
@@ -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.
|
agent_memory_git-0.1.9/src/agent_memory/demo/personal/2026/09/20260906-push-personal-repos-w8re.md
ADDED
|
@@ -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
|
|
4
|
-
memory, the way the hooks write them, and opens the page on
|
|
5
|
-
project site's live demo is built from the same memories
|
|
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
|
-
|
|
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
|
|
39
|
-
"""
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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(
|
|
111
|
-
h.update(str(path.relative_to(
|
|
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(
|
|
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:
|