agent-memory-git 0.1.10__tar.gz → 0.1.11__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.10 → agent_memory_git-0.1.11}/PKG-INFO +3 -3
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/README.md +2 -2
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/pyproject.toml +1 -1
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/pyproject.toml.orig +1 -1
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/PROTOCOL.md +1 -1
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/cli.py +15 -4
- agent_memory_git-0.1.11/src/agent_memory/hooks.py +141 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/install.py +5 -1
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/ui_server.py +6 -1
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/app.css +13 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/main.js +1 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/store.js +22 -5
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/overlays.js +3 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/profiles.js +11 -1
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/settings.js +1 -1
- agent_memory_git-0.1.10/src/agent_memory/hooks.py +0 -122
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/LICENSE +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/__init__.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/__main__.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/capture.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/config.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/context.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/08/20260816-blog-publishing-k3qm.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/09/20260906-push-personal-repos-w8re.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/09/20260921-home-server-n2vb.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/10/20261004-options-first-7hcx.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/personal/2026/10/20261009-thesis-setup-fr6e.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260804-alex-profile-ujzd.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260804-reply-language-xdnc.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260812-ask-before-push-pf3d.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260820-tests-before-done-odzd.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260827-etl-orders-export-deadline-pft7.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/08/20260828-etl-dashboard-72ce.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260902-report-eta-is2j.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260908-shop-web-stripe-checkout-lgmx.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260910-shop-web-deploys-dn73.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260915-etl-parquet-partitioning-seh2.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/09/20260922-design-tokens-source-uvw7.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/10/20261002-shop-web-v3-freeze-5xtp.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo/work/2026/10/20261012-payments-runbook-4syw.md +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/demo.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/embed_worker.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/frontmatter.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/gitsync.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/importers.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/lint.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/profiles.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/select.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/sessions.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/store.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/ui.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/update.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/util.py +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/index.html +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/api.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/avatar.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/filters.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/format.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/h.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/icons.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/markdown.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/search.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/insights.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/list.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/memory.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/shell.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/ui.js +0 -0
- {agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/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.11
|
|
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,8 +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
|
|
62
|
-
memories in repositories of their own.
|
|
61
|
+
[Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep separate sets of
|
|
62
|
+
memories in repositories of their own, and a switch reaches running sessions too.
|
|
63
63
|
- **Nothing to run.** Two hooks and git. No service, no account; search by meaning runs on your CPU.
|
|
64
64
|
- **A page to keep it tidy.** `mem ui` searches by words and by meaning, edits with undo, and shows
|
|
65
65
|
what changed week by week.
|
|
@@ -37,8 +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
|
|
41
|
-
memories in repositories of their own.
|
|
40
|
+
[Profiles](https://ki-seki.github.io/agent-memory/docs.html#profiles) keep separate sets of
|
|
41
|
+
memories in repositories of their own, and a switch reaches running sessions too.
|
|
42
42
|
- **Nothing to run.** Two hooks and git. No service, no account; search by meaning runs on your CPU.
|
|
43
43
|
- **A page to keep it tidy.** `mem ui` searches by words and by meaning, edits with undo, and shows
|
|
44
44
|
what changed week by week.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Shared memory
|
|
2
2
|
|
|
3
|
-
You have a persistent memory that Claude Code and Codex share. Every memory is one Markdown file in a git repository. At the start of each session a hook injects a block titled "Shared memory index": it lists the memories relevant to the current folder and repository, and gives the memories directory, this session's staging directory and the path of the `mem` command. If that block is missing, the memory system is not active in this session; ignore this section.
|
|
3
|
+
You have a persistent memory that Claude Code and Codex share. Every memory is one Markdown file in a git repository. At the start of each session a hook injects a block titled "Shared memory index": it lists the memories relevant to the current folder and repository, and gives the memories directory, this session's staging directory and the path of the `mem` command. If the active profile changes while the session runs, a new index arrives with your next message and replaces the earlier one: from then on use only its memories and paths. If that block is missing, the memory system is not active in this session; ignore this section.
|
|
4
4
|
|
|
5
5
|
## Reading
|
|
6
6
|
|
|
@@ -249,25 +249,36 @@ def _access_note():
|
|
|
249
249
|
print(f"updated {' and '.join(tools)} so agents can reach every profile's folder")
|
|
250
250
|
|
|
251
251
|
|
|
252
|
+
SWITCH_NOTE = "they keep what they've already read; start a new session for a clean slate"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _switched(name):
|
|
256
|
+
print(f"sessions now use {name}; running ones switch with their next message")
|
|
257
|
+
if config.load().get("switch_note", True):
|
|
258
|
+
print(SWITCH_NOTE)
|
|
259
|
+
|
|
260
|
+
|
|
252
261
|
def cmd_profile(args):
|
|
253
262
|
if profiles.locked():
|
|
254
263
|
sys.exit("AGENT_MEMORY_DATA picks the data repository here, so profiles do not apply")
|
|
255
264
|
try:
|
|
256
265
|
if args.action == "use":
|
|
257
266
|
pid = profiles.use(args.profile)
|
|
258
|
-
|
|
267
|
+
_switched(profiles.listing()[0][pid]["name"])
|
|
259
268
|
elif args.action == "add":
|
|
260
269
|
pid, note = profiles.add(args.folder, remote=args.remote, switch=not args.keep)
|
|
261
270
|
found, active = profiles.listing()
|
|
262
271
|
print(note)
|
|
263
|
-
print(f"added {found[pid]['name']}"
|
|
272
|
+
print(f"added {found[pid]['name']}")
|
|
273
|
+
if active == pid:
|
|
274
|
+
_switched(found[pid]["name"])
|
|
264
275
|
_access_note()
|
|
265
276
|
elif args.action == "remove":
|
|
266
277
|
found, _ = profiles.listing()
|
|
267
278
|
pid = profiles.find(args.profile, found)
|
|
268
279
|
active = profiles.remove(pid)
|
|
269
280
|
print(f"removed {found[pid]['name']}; {profiles.short(found[pid]['data_dir'])} is left as it is")
|
|
270
|
-
|
|
281
|
+
_switched(profiles.listing()[0][active]["name"])
|
|
271
282
|
_access_note()
|
|
272
283
|
else:
|
|
273
284
|
for p in profiles.summary()["profiles"]:
|
|
@@ -387,7 +398,7 @@ def main(argv=None):
|
|
|
387
398
|
sp.set_defaults(fn=cmd_profile)
|
|
388
399
|
|
|
389
400
|
sp = sub.add_parser("hook", help=argparse.SUPPRESS)
|
|
390
|
-
sp.add_argument("event"
|
|
401
|
+
sp.add_argument("event") # any name: an event from a newer install is logged and ignored, never refused
|
|
391
402
|
sp.add_argument("--tool", required=True, choices=["claude-code", "codex"])
|
|
392
403
|
sp.set_defaults(fn=cmd_hook)
|
|
393
404
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Hook entry points shared by Claude Code and Codex.
|
|
2
|
+
|
|
3
|
+
Both tools send a JSON payload on stdin with at least `session_id` and `cwd`,
|
|
4
|
+
and accept `hookSpecificOutput.additionalContext` from SessionStart hooks.
|
|
5
|
+
A hook must never break the host session, so every failure is logged and
|
|
6
|
+
swallowed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import secrets
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from . import capture, config, gitsync, profiles, select, store, workdir
|
|
17
|
+
from .context import detect
|
|
18
|
+
from .util import log, log_exc
|
|
19
|
+
|
|
20
|
+
PULL_INTERVAL = 60
|
|
21
|
+
SWITCHED = (
|
|
22
|
+
"The active memory profile changed while this session was running. This index replaces the one shown "
|
|
23
|
+
"earlier: from now on use only its memories directory, its staging directory and the memories it lists, "
|
|
24
|
+
"and set aside memories you read under the earlier one.\n\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _payload():
|
|
29
|
+
try:
|
|
30
|
+
data = sys.stdin.read()
|
|
31
|
+
return json.loads(data) if data.strip() else {}
|
|
32
|
+
except (OSError, ValueError):
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _session_id(payload):
|
|
37
|
+
return str(payload.get("session_id") or f"nosession-{secrets.token_hex(4)}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def session_start(payload, tool, data_dir):
|
|
41
|
+
sid = _session_id(payload)
|
|
42
|
+
ctx = detect(payload.get("cwd"))
|
|
43
|
+
sdir = capture.session_dir(data_dir, tool, sid)
|
|
44
|
+
sdir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
capture.write_session(
|
|
46
|
+
sdir,
|
|
47
|
+
{
|
|
48
|
+
"tool": tool,
|
|
49
|
+
"session_id": sid,
|
|
50
|
+
"session_title": payload.get("session_title"),
|
|
51
|
+
"model": payload.get("model"),
|
|
52
|
+
"transcript_path": payload.get("transcript_path"),
|
|
53
|
+
"ctx": ctx,
|
|
54
|
+
"shown_at": time.time(),
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
recovered = capture.recover_orphans(data_dir, keep=sdir)
|
|
59
|
+
stamp = Path(data_dir) / ".cache" / "last-pull"
|
|
60
|
+
if gitsync.has_remote(data_dir):
|
|
61
|
+
last = stamp.stat().st_mtime if stamp.exists() else 0
|
|
62
|
+
if recovered or time.time() - last > PULL_INTERVAL:
|
|
63
|
+
stamp.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
stamp.touch()
|
|
65
|
+
gitsync.spawn_sync(data_dir, push=recovered)
|
|
66
|
+
|
|
67
|
+
paths = {
|
|
68
|
+
"memories": str(store.memories_dir(data_dir)),
|
|
69
|
+
"staging": str(sdir),
|
|
70
|
+
"mem": str(config.BIN),
|
|
71
|
+
}
|
|
72
|
+
return select.render(
|
|
73
|
+
store.load_all(data_dir),
|
|
74
|
+
ctx,
|
|
75
|
+
paths,
|
|
76
|
+
throwaway=workdir.is_throwaway(ctx.get("cwd")),
|
|
77
|
+
profile=profiles.active_name(),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _shown(sdir):
|
|
82
|
+
"""When this staging folder's profile last gave the session its index; 0 when it never did."""
|
|
83
|
+
info = capture.read_session(sdir)
|
|
84
|
+
if isinstance(info.get("shown_at"), (int, float)):
|
|
85
|
+
return info["shown_at"]
|
|
86
|
+
try: # sessions that started before shown_at existed got their index when the folder was made
|
|
87
|
+
return datetime.fromisoformat(info["started_at"]).timestamp()
|
|
88
|
+
except (KeyError, TypeError, ValueError):
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def prompt(payload, tool, data_dir):
|
|
93
|
+
"""The active profile's index when the session last got another profile's; None when nothing changed.
|
|
94
|
+
Running sessions follow the active profile, so a switch reaches them with their next message."""
|
|
95
|
+
sid = payload.get("session_id")
|
|
96
|
+
if not sid:
|
|
97
|
+
return None
|
|
98
|
+
seen = [(_shown(sdir), d) for d in profiles.dirs() if (sdir := capture.session_dir(d, tool, str(sid))).is_dir()]
|
|
99
|
+
if not seen or max(seen, key=lambda s: s[0])[1] == data_dir:
|
|
100
|
+
return None # no index yet (SessionStart gives it), or this profile's is the latest
|
|
101
|
+
return SWITCHED + session_start(payload, tool, data_dir)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def flush(payload, tool, final):
|
|
105
|
+
"""Commit what the session staged, into each profile it staged in: a switch can leave some in the one
|
|
106
|
+
before, written while the session still had that profile's index."""
|
|
107
|
+
sid = _session_id(payload)
|
|
108
|
+
for data_dir in profiles.dirs():
|
|
109
|
+
sdir = capture.session_dir(data_dir, tool, sid)
|
|
110
|
+
if not sdir.is_dir() or not (data_dir / ".git").exists():
|
|
111
|
+
continue
|
|
112
|
+
if payload.get("transcript_path"):
|
|
113
|
+
capture.write_session(sdir, {"transcript_path": payload["transcript_path"]})
|
|
114
|
+
committed = capture.flush(data_dir, sdir, final=final, lock_timeout=1.0 if final else 5.0)
|
|
115
|
+
if committed and gitsync.has_remote(data_dir):
|
|
116
|
+
gitsync.spawn_sync(data_dir, push=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _context(event, text):
|
|
120
|
+
return json.dumps({"hookSpecificOutput": {"hookEventName": event, "additionalContext": text}}, ensure_ascii=False)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(event, tool):
|
|
124
|
+
payload = _payload()
|
|
125
|
+
data_dir = config.data_dir() # the active profile's: running sessions follow it too
|
|
126
|
+
if not data_dir or not (data_dir / ".git").exists():
|
|
127
|
+
return 0
|
|
128
|
+
try:
|
|
129
|
+
if event == "session-start":
|
|
130
|
+
print(_context("SessionStart", session_start(payload, tool, data_dir)))
|
|
131
|
+
elif event == "prompt":
|
|
132
|
+
text = prompt(payload, tool, data_dir)
|
|
133
|
+
if text:
|
|
134
|
+
print(_context("UserPromptSubmit", text))
|
|
135
|
+
elif event in ("stop", "session-end"):
|
|
136
|
+
flush(payload, tool, final=event == "session-end")
|
|
137
|
+
else:
|
|
138
|
+
log(data_dir, f"unknown hook event {event!r}")
|
|
139
|
+
except Exception:
|
|
140
|
+
log_exc(data_dir, f"hook {event} ({tool})")
|
|
141
|
+
return 0
|
|
@@ -35,6 +35,7 @@ PATH_LINE = 'export PATH="$HOME/.local/bin:$PATH"'
|
|
|
35
35
|
|
|
36
36
|
HOOK_EVENTS = [
|
|
37
37
|
("SessionStart", "session-start", 15),
|
|
38
|
+
("UserPromptSubmit", "prompt", 10),
|
|
38
39
|
("Stop", "stop", 15),
|
|
39
40
|
("SessionEnd", "session-end", 3),
|
|
40
41
|
] # Codex clamps SessionEnd to 3s
|
|
@@ -44,7 +45,9 @@ HOOK_EVENTS = [
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def hook_command(event, tool):
|
|
47
|
-
|
|
48
|
+
command = " ".join(shlex.quote(a) for a in config.mem_command()) + f" hook {event} --tool {tool}"
|
|
49
|
+
# a mem from before this event exits 2 on it, and exit 2 from UserPromptSubmit blocks the prompt
|
|
50
|
+
return command + " || true" if event == "prompt" else command
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
def _is_ours(group):
|
|
@@ -318,6 +321,7 @@ def _codex_plan(plan, data_dirs, state):
|
|
|
318
321
|
handler = {"type": "command", "command": hook_command(name, "codex"), "timeout": timeout}
|
|
319
322
|
if event == "SessionStart":
|
|
320
323
|
handler["statusMessage"] = "Loading shared memory"
|
|
324
|
+
if event in ("SessionStart", "UserPromptSubmit"):
|
|
321
325
|
handler["additionalContextLimit"] = 0 # keep the index inline, never spill to disk
|
|
322
326
|
groups.append({"hooks": [handler]})
|
|
323
327
|
hooks[event] = groups
|
|
@@ -173,6 +173,7 @@ class UiApp:
|
|
|
173
173
|
"remoteUrl": re.sub(r"(://)[^/@]+@", r"\1", url) or None, # without any credentials in it
|
|
174
174
|
"sync": self.sync_status(),
|
|
175
175
|
"keywordStopwords": config.load().get("keyword_stopwords", []),
|
|
176
|
+
"switchNote": config.load().get("switch_note", True),
|
|
176
177
|
"model": {
|
|
177
178
|
"name": ui.MODEL["name"],
|
|
178
179
|
"ready": ui.model_ready(),
|
|
@@ -185,7 +186,7 @@ class UiApp:
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
def set_settings(self, p):
|
|
188
|
-
"""Change keyword_stopwords, check_updates, or
|
|
189
|
+
"""Change keyword_stopwords, check_updates, switch_note, or several."""
|
|
189
190
|
cfg = config.load()
|
|
190
191
|
if "keyword_stopwords" in p:
|
|
191
192
|
words = p["keyword_stopwords"]
|
|
@@ -196,6 +197,10 @@ class UiApp:
|
|
|
196
197
|
if not isinstance(p["check_updates"], bool):
|
|
197
198
|
raise ApiError(400, "check_updates must be true or false")
|
|
198
199
|
cfg["check_updates"] = p["check_updates"]
|
|
200
|
+
if "switch_note" in p:
|
|
201
|
+
if not isinstance(p["switch_note"], bool):
|
|
202
|
+
raise ApiError(400, "switch_note must be true or false")
|
|
203
|
+
cfg["switch_note"] = p["switch_note"]
|
|
199
204
|
config.save(cfg)
|
|
200
205
|
if "keyword_stopwords" in p:
|
|
201
206
|
self.refresh() # keywords are chosen when the state is built
|
|
@@ -617,6 +617,19 @@ button.hero-meta-item:hover .copy-hint, button.hero-meta-item:focus-visible .cop
|
|
|
617
617
|
.dialog::backdrop { background: rgb(0 0 0 / .3); }
|
|
618
618
|
.dialog-title { font-size: 15px; font-weight: 600; }
|
|
619
619
|
.dialog-body { margin-top: 8px; font-size: 13.5px; white-space: pre-line; color: var(--ink-2); }
|
|
620
|
+
.sline { display: grid; grid-template-columns: auto auto; justify-content: start; margin-top: 14px; font-size: 12px; color: var(--ink-3); }
|
|
621
|
+
.sline > span { padding-right: 14px; }
|
|
622
|
+
.sline > .next:not(.sline-name) { padding-left: 14px; border-left: 1.5px dashed var(--ink-3); }
|
|
623
|
+
.sline > .sline-name.next { padding-left: 15.5px; }
|
|
624
|
+
.sline > .sline-when { padding-bottom: 4px; }
|
|
625
|
+
.sline-msgs { display: flex; align-items: center; gap: 5px; height: 22px; }
|
|
626
|
+
.sline-msgs i { width: 20px; height: 11px; border-radius: 6px; overflow: hidden; box-shadow: 0 0 0 0.5px var(--line); }
|
|
627
|
+
.sline-msgs i svg { display: block; width: 100%; height: 100%; }
|
|
628
|
+
.sline-msgs b { font-weight: 400; margin-left: 2px; }
|
|
629
|
+
.sline-name { display: flex; align-items: center; gap: 6px; margin-top: 6px; color: var(--ink-2); }
|
|
630
|
+
.sline-name .pavatar { width: 16px; height: 16px; }
|
|
631
|
+
.dialog-check { display: flex; align-items: center; gap: 8px; margin-top: 14px; padding: 0; border: 0; background: none; color: var(--ink-2); font: inherit; font-size: 13px; cursor: pointer; }
|
|
632
|
+
.dialog-check[aria-checked="true"] .check { border-color: var(--accent); background: var(--accent); color: var(--accent-ink); }
|
|
620
633
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
|
|
621
634
|
.dialog.wide { width: min(1040px, calc(100vw - 32px)); max-height: min(760px, calc(100vh - 48px)); padding: 0; }
|
|
622
635
|
.dialog.wide[open] { display: flex; flex-direction: column; }
|
|
@@ -102,6 +102,7 @@ const ACTIONS = {
|
|
|
102
102
|
removePicked: () => app.remove([...app.picked]),
|
|
103
103
|
undoToast: (id) => { const t = app.toasts.find((x) => String(x.id) === id); if (t) { app.dismiss(t.id); app.undo(t.undo); } },
|
|
104
104
|
answer: (v) => app.answer(v === "yes"),
|
|
105
|
+
askCheck: () => { if (app.asking) { app.asking.checked = !app.asking.checked; app.changed(); } },
|
|
105
106
|
compare: (i) => { app.comparing = duplicatePairs()[Number(i)] ?? null; app.changed(); },
|
|
106
107
|
closeCompare: () => { app.comparing = null; app.changed(); },
|
|
107
108
|
removeCompared: (id) => { app.comparing = null; app.remove([id]); },
|
|
@@ -24,6 +24,9 @@ const SORTERS = {
|
|
|
24
24
|
az: (a, b) => a.description.localeCompare(b.description),
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
/** What switching profiles means for the sessions that are running. */
|
|
28
|
+
const SWITCH_NOTE = "What a session has already read stays with it. Start a new session for a clean slate.";
|
|
29
|
+
|
|
27
30
|
class App {
|
|
28
31
|
data = null;
|
|
29
32
|
bootError = "";
|
|
@@ -385,13 +388,26 @@ class App {
|
|
|
385
388
|
if (this.profiles?.active === id) return this.pform && this.cancelProfile();
|
|
386
389
|
if (!(await this.#leaveEdit())) return;
|
|
387
390
|
const to = this.profiles?.profiles.find((p) => p.id === id);
|
|
391
|
+
if (!(await this.#switchOk(to?.name ?? id))) return;
|
|
388
392
|
this.switching = id; this.changed();
|
|
389
393
|
try {
|
|
390
394
|
this.#took(await api("/api/profiles/use", { id }));
|
|
391
|
-
this.toast(`Switched to ${to?.name ?? id}
|
|
395
|
+
this.toast(`Switched to ${to?.name ?? id}`);
|
|
392
396
|
} catch (e) { this.toast(e.message, { error: true }); }
|
|
393
397
|
finally { this.switching = ""; this.changed(); }
|
|
394
398
|
}
|
|
399
|
+
/** Whether to go on with a switch, once told what it means for sessions that are running; until told not to say. */
|
|
400
|
+
async #switchOk(name) {
|
|
401
|
+
if (this.profiles?.demo || this.settings.s?.switchNote === false) return true;
|
|
402
|
+
const from = this.profiles?.profiles.find((p) => p.active)?.name;
|
|
403
|
+
const q = { title: `Switch to ${name}?`, timeline: from && { from, to: name }, body: SWITCH_NOTE, action: "Switch", check: "Don't show this again" };
|
|
404
|
+
if (!(await this.ask(q))) return false;
|
|
405
|
+
if (q.checked) {
|
|
406
|
+
try { this.settings.s = await api("/api/settings", { switch_note: false }); }
|
|
407
|
+
catch (e) { this.toast(e.message, { error: true }); }
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
395
411
|
/** After the server answers a profile change: its list, and the memories of the profile it now shows. */
|
|
396
412
|
async #took(r) {
|
|
397
413
|
const before = this.profiles?.active;
|
|
@@ -423,20 +439,20 @@ class App {
|
|
|
423
439
|
}
|
|
424
440
|
async saveProfile() {
|
|
425
441
|
const f = this.pform;
|
|
426
|
-
if (!f || f.busy) return;
|
|
442
|
+
if (!f || f.busy || !(await this.#switchOk(f.name))) return;
|
|
427
443
|
f.busy = true; f.error = ""; this.changed();
|
|
428
444
|
try {
|
|
429
445
|
const r = await api("/api/profiles/add", { name: f.name, folder: f.folder, remote: f.remote.trim() || null });
|
|
430
446
|
const made = r.profiles.find((p) => p.id === r.id)?.name ?? f.name;
|
|
431
447
|
await this.#took(r);
|
|
432
|
-
this.toast(
|
|
448
|
+
this.toast(`${made}: ${r.message}`);
|
|
433
449
|
} catch (e) { f.busy = false; f.error = e.message; this.changed(); }
|
|
434
450
|
}
|
|
435
451
|
/** Forget the profile the page shows, after asking; its folder and remote stay. */
|
|
436
452
|
async removeProfile() {
|
|
437
453
|
const p = this.profiles?.profiles.find((x) => x.active);
|
|
438
454
|
if (!p) return;
|
|
439
|
-
const ok = await this.ask({ title: `Remove the ${p.name} profile?`, body: `agent-memory stops using it. The folder ${p.dataDir} and its memories stay on disk, and any remote is not touched; add the folder again later to bring it back.`, action: "Remove", danger: true });
|
|
455
|
+
const ok = await this.ask({ title: `Remove the ${p.name} profile?`, body: `agent-memory stops using it. The folder ${p.dataDir} and its memories stay on disk, and any remote is not touched; add the folder again later to bring it back. Running sessions move to the profile left active with their next message.`, action: "Remove", danger: true });
|
|
440
456
|
if (!ok) return;
|
|
441
457
|
try { await this.#took(await api("/api/profiles/remove", { id: p.id })); this.toast(`Removed ${p.name}`); }
|
|
442
458
|
catch (e) { this.toast(e.message, { error: true }); }
|
|
@@ -453,7 +469,8 @@ class App {
|
|
|
453
469
|
this.changed();
|
|
454
470
|
}
|
|
455
471
|
dismiss(id) { this.toasts = this.toasts.filter((t) => t.id !== id); this.changed(); }
|
|
456
|
-
|
|
472
|
+
/** Ask in a dialog; resolves true or false, and with `check` set leaves the box's state in q.checked. */
|
|
473
|
+
ask(q) { return new Promise((resolve) => { this.asking = Object.assign(q, { resolve }); this.changed(); }); }
|
|
457
474
|
answer(ok) { const a = this.asking; this.asking = null; a?.resolve(ok); this.changed(); }
|
|
458
475
|
}
|
|
459
476
|
|
{agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/overlays.js
RENAMED
|
@@ -4,6 +4,7 @@ import { html } from "../h.js";
|
|
|
4
4
|
import { icon } from "../icons.js";
|
|
5
5
|
import { parse, renderBlocks } from "../markdown.js";
|
|
6
6
|
import { app } from "../store.js";
|
|
7
|
+
import { switchLine } from "./profiles.js";
|
|
7
8
|
import { typeDot } from "./ui.js";
|
|
8
9
|
|
|
9
10
|
export function selectionBar() {
|
|
@@ -29,7 +30,9 @@ export function confirmDialog() {
|
|
|
29
30
|
const a = app.asking;
|
|
30
31
|
if (!a) return "";
|
|
31
32
|
return html`<h2 class="dialog-title">${a.title}</h2>
|
|
33
|
+
${a.timeline ? switchLine(a.timeline.from, a.timeline.to) : ""}
|
|
32
34
|
${a.body ? html`<p class="dialog-body">${a.body}</p>` : ""}
|
|
35
|
+
${a.check ? html`<button type="button" role="checkbox" class="dialog-check" aria-checked="${String(!!a.checked)}" data-act="askCheck"><span class="check" aria-hidden="true">${a.checked ? icon("check", "", 3) : ""}</span>${a.check}</button>` : ""}
|
|
33
36
|
<div class="dialog-actions">
|
|
34
37
|
<button type="button" class="btn" data-act="answer" data-arg="no">Cancel</button>
|
|
35
38
|
<button type="button" class="${a.danger ? "btn danger" : "btn primary"}" data-act="answer" data-arg="yes" autofocus>${a.action}</button>
|
{agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/profiles.js
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Profiles: separate memory repositories, each its own folder and remote. Settings opens on the active one, with
|
|
2
2
|
// the others in its foot; the rail's last button is the active profile's picture, which leads there. A profile's name is picked for it when it is made, and its picture is
|
|
3
3
|
// drawn from the name.
|
|
4
|
-
import { avatar } from "../avatar.js";
|
|
4
|
+
import { avatar, glow } from "../avatar.js";
|
|
5
5
|
import { attrs, html } from "../h.js";
|
|
6
6
|
import { icon } from "../icons.js";
|
|
7
7
|
import { app } from "../store.js";
|
|
@@ -43,6 +43,16 @@ export function newProfile() {
|
|
|
43
43
|
</form>`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** An open session as a row of messages: those so far in `from`'s colours, the next ones in `to`'s. */
|
|
47
|
+
export function switchLine(from, to) {
|
|
48
|
+
const msgs = (name, n) => Array.from({ length: n }, () => html`<i>${glow(name)}</i>`);
|
|
49
|
+
return html`<div class="sline" role="img"${attrs({ "aria-label": `An open session: its messages so far used ${from}; from its next message it uses ${to}` })}>
|
|
50
|
+
<span class="sline-when">So far</span><span class="sline-when next">Next message</span>
|
|
51
|
+
<span class="sline-msgs">${msgs(from, 3)}</span><span class="sline-msgs next">${msgs(to, 2)}<b>…</b></span>
|
|
52
|
+
<span class="sline-name">${avatar(from)}${from}</span><span class="sline-name next">${avatar(to)}${to}</span>
|
|
53
|
+
</div>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
// ---------------------------------------------------------------- the rail
|
|
47
57
|
|
|
48
58
|
/** The rail's last button: the active profile's picture, which opens Settings, where profiles are switched. Without
|
{agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/settings.js
RENAMED
|
@@ -52,7 +52,7 @@ function hero() {
|
|
|
52
52
|
const s = app.settings.s, st = app.settings, p = active();
|
|
53
53
|
const name = p?.name ?? s?.name ?? "agent-memory";
|
|
54
54
|
const count = `${app.M.length} ${app.M.length === 1 ? "memory" : "memories"}`;
|
|
55
|
-
const role = !p ? "" : app.profiles.demo ? "The profile the page shows" : "
|
|
55
|
+
const role = !p ? "" : app.profiles.demo ? "The profile the page shows" : "Sessions use this profile";
|
|
56
56
|
const note = st.remoteNote ? html`<p${attrs({ class: cx("hero-note", !st.remoteNote.ok && "bad"), role: "status" })}>${st.remoteNote.text}</p>` : "";
|
|
57
57
|
return html`<section class="hero" data-key="hero" aria-label="${p ? "The open profile" : "The data repository"}">
|
|
58
58
|
<div class="hero-glow" aria-hidden="true">${glow(name)}</div>
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""Hook entry points shared by Claude Code and Codex.
|
|
2
|
-
|
|
3
|
-
Both tools send a JSON payload on stdin with at least `session_id` and `cwd`,
|
|
4
|
-
and accept `hookSpecificOutput.additionalContext` from SessionStart hooks.
|
|
5
|
-
A hook must never break the host session, so every failure is logged and
|
|
6
|
-
swallowed.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import secrets
|
|
11
|
-
import sys
|
|
12
|
-
import time
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
from . import capture, config, gitsync, profiles, select, store, workdir
|
|
16
|
-
from .context import detect
|
|
17
|
-
from .util import log, log_exc
|
|
18
|
-
|
|
19
|
-
PULL_INTERVAL = 60
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _payload():
|
|
23
|
-
try:
|
|
24
|
-
data = sys.stdin.read()
|
|
25
|
-
return json.loads(data) if data.strip() else {}
|
|
26
|
-
except (OSError, ValueError):
|
|
27
|
-
return {}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _session_id(payload):
|
|
31
|
-
return str(payload.get("session_id") or f"nosession-{secrets.token_hex(4)}")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def session_start(payload, tool, data_dir):
|
|
35
|
-
sid = _session_id(payload)
|
|
36
|
-
ctx = detect(payload.get("cwd"))
|
|
37
|
-
sdir = capture.session_dir(data_dir, tool, sid)
|
|
38
|
-
sdir.mkdir(parents=True, exist_ok=True)
|
|
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
|
-
)
|
|
50
|
-
|
|
51
|
-
recovered = capture.recover_orphans(data_dir, keep=sdir)
|
|
52
|
-
stamp = Path(data_dir) / ".cache" / "last-pull"
|
|
53
|
-
if gitsync.has_remote(data_dir):
|
|
54
|
-
last = stamp.stat().st_mtime if stamp.exists() else 0
|
|
55
|
-
if recovered or time.time() - last > PULL_INTERVAL:
|
|
56
|
-
stamp.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
-
stamp.touch()
|
|
58
|
-
gitsync.spawn_sync(data_dir, push=recovered)
|
|
59
|
-
|
|
60
|
-
paths = {
|
|
61
|
-
"memories": str(store.memories_dir(data_dir)),
|
|
62
|
-
"staging": str(sdir),
|
|
63
|
-
"mem": str(config.BIN),
|
|
64
|
-
}
|
|
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
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def flush(payload, tool, data_dir, final):
|
|
75
|
-
sdir = capture.session_dir(data_dir, tool, _session_id(payload))
|
|
76
|
-
if not sdir.is_dir():
|
|
77
|
-
return
|
|
78
|
-
if payload.get("transcript_path"):
|
|
79
|
-
capture.write_session(sdir, {"transcript_path": payload["transcript_path"]})
|
|
80
|
-
committed = capture.flush(data_dir, sdir, final=final, lock_timeout=1.0 if final else 5.0)
|
|
81
|
-
if committed and gitsync.has_remote(data_dir):
|
|
82
|
-
gitsync.spawn_sync(data_dir, push=True)
|
|
83
|
-
|
|
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
|
-
|
|
95
|
-
def main(event, tool):
|
|
96
|
-
payload = _payload()
|
|
97
|
-
data_dir = config.data_dir()
|
|
98
|
-
if data_dir and event != "session-start":
|
|
99
|
-
data_dir = _home(payload, tool, data_dir)
|
|
100
|
-
if not data_dir or not (data_dir / ".git").exists():
|
|
101
|
-
return 0
|
|
102
|
-
try:
|
|
103
|
-
if event == "session-start":
|
|
104
|
-
text = session_start(payload, tool, data_dir)
|
|
105
|
-
print(
|
|
106
|
-
json.dumps(
|
|
107
|
-
{
|
|
108
|
-
"hookSpecificOutput": {
|
|
109
|
-
"hookEventName": "SessionStart",
|
|
110
|
-
"additionalContext": text,
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
ensure_ascii=False,
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
elif event in ("stop", "session-end"):
|
|
117
|
-
flush(payload, tool, data_dir, final=event == "session-end")
|
|
118
|
-
else:
|
|
119
|
-
log(data_dir, f"unknown hook event {event!r}")
|
|
120
|
-
except Exception:
|
|
121
|
-
log_exc(data_dir, f"hook {event} ({tool})")
|
|
122
|
-
return 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/insights.js
RENAMED
|
File without changes
|
|
File without changes
|
{agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/memory.js
RENAMED
|
File without changes
|
{agent_memory_git-0.1.10 → agent_memory_git-0.1.11}/src/agent_memory/webui/js/views/shell.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|