agent-memory-git 0.1.3__tar.gz → 0.1.5__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.3 → agent_memory_git-0.1.5}/PKG-INFO +35 -12
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/README.md +34 -11
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/pyproject.toml +1 -1
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/pyproject.toml.orig +1 -1
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/config.py +13 -1
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/install.py +12 -6
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/ui_server.py +179 -12
- agent_memory_git-0.1.5/src/agent_memory/update.py +211 -0
- agent_memory_git-0.1.5/src/agent_memory/webui/assets/index-MdftBYNd.js +19 -0
- agent_memory_git-0.1.5/src/agent_memory/webui/assets/index-h-rsgD7i.css +1 -0
- agent_memory_git-0.1.5/src/agent_memory/webui/index.html +14 -0
- agent_memory_git-0.1.3/src/agent_memory/webui/app.css +0 -263
- agent_memory_git-0.1.3/src/agent_memory/webui/app.js +0 -739
- agent_memory_git-0.1.3/src/agent_memory/webui/index.html +0 -47
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/LICENSE +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/PROTOCOL.md +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/__init__.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/__main__.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/capture.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/cli.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/context.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/embed_worker.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/frontmatter.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/gitsync.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/hooks.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/importers.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/lint.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/select.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/sessions.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/store.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/ui.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/src/agent_memory/util.py +0 -0
- {agent_memory_git-0.1.3 → agent_memory_git-0.1.5}/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.5
|
|
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
|
|
@@ -88,7 +88,8 @@ mem install
|
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
To upgrade, run `uv tool upgrade agent-memory-git` and then `mem install`, which refreshes the
|
|
91
|
-
protocol copies; `mem doctor` says when they are out of date.
|
|
91
|
+
protocol copies; `mem doctor` says when they are out of date. `mem ui` does both from Settings, with
|
|
92
|
+
one click, when a newer release is out.
|
|
92
93
|
|
|
93
94
|
`install` edits, and records in `~/.config/agent-memory/install-state.json`:
|
|
94
95
|
|
|
@@ -125,22 +126,33 @@ mem doctor check the installation
|
|
|
125
126
|
|
|
126
127
|
## Managing memories in a page
|
|
127
128
|
|
|
128
|
-
`mem ui` starts a small server on `127.0.0.1` and opens a page
|
|
129
|
-
terminal open while you use it. It
|
|
129
|
+
`mem ui` starts a small server on `127.0.0.1` and opens a page with three parts: Memories, Insights
|
|
130
|
+
and Settings (the logo always leads back to Memories). Keep the terminal open while you use it. It
|
|
131
|
+
needs `numpy`; the hooks do not.
|
|
130
132
|
|
|
131
133
|
- **Find.** One search box (press `/`). Word matches appear as you type, with the matching words
|
|
132
134
|
marked; a moment later the local model adds memories close in meaning, across Chinese and
|
|
133
135
|
English. Filters narrow the list by type, where a memory loads (everywhere, or one project or
|
|
134
136
|
folder), which tool wrote it, keyword, when it was added (a recent period or a date range), and
|
|
135
137
|
status (needs tidying, as `mem lint` reports and the open memory lists, or unchanged for 60
|
|
136
|
-
days). Tick several values in one filter to see memories that match any of them. The
|
|
137
|
-
|
|
138
|
-
- **Read.** Click a
|
|
139
|
-
the body rendered as Markdown
|
|
140
|
-
|
|
138
|
+
days). Tick several values in one filter to see memories that match any of them. The list's
|
|
139
|
+
header sets the order: newest, oldest, recently changed or A to Z.
|
|
140
|
+
- **Read.** Click a memory, or move with `j`/`k`. It opens beside the list (drag the line between
|
|
141
|
+
them to resize) with the body rendered as Markdown, `[[links]]` that open other memories, where
|
|
142
|
+
it came from, every other frontmatter field, the most similar memories, and the file exactly as
|
|
143
|
+
stored. On a narrow window the list and the memory take turns.
|
|
141
144
|
- **Change.** Edit (`e`) or delete from the open memory, add one with New (`n`), or tick several
|
|
142
|
-
|
|
143
|
-
with Undo. If an agent edits the same file meanwhile, saving is refused rather than
|
|
145
|
+
memories (`x`) and delete them together. Every change is one git commit, pushed in the
|
|
146
|
+
background, with Undo. If an agent edits the same file meanwhile, saving is refused rather than
|
|
147
|
+
overwriting.
|
|
148
|
+
- **Insights.** How many memories there are and how they change week by week, where they load and
|
|
149
|
+
which tool wrote them, pairs that read alike (compare them side by side and keep one), and the
|
|
150
|
+
index a session would get at a chosen place, against the 10,000-character hook limit.
|
|
151
|
+
- **Settings.** Light or dark, updates, the data repository and a sync on demand, words never to
|
|
152
|
+
use as keywords, the embedding model, and the same checks as `mem doctor`. Once a day the page
|
|
153
|
+
asks PyPI whether a newer release is out (only the package name is sent; turn it off there); when
|
|
154
|
+
`uv tool` installed agent-memory, Update runs the upgrade and `mem install` and restarts the page
|
|
155
|
+
on the new version.
|
|
144
156
|
|
|
145
157
|
Similar memories and search by meaning use Qwen3-Embedding-0.6B, int8 ONNX, on the CPU, in a
|
|
146
158
|
separate `uv run --with onnxruntime --with tokenizers` environment; vectors are cached by content
|
|
@@ -149,7 +161,7 @@ pinned revision, checked by SHA-256). Without it, or with `--keywords-only`, sim
|
|
|
149
161
|
come from keyword vectors and search matches words only.
|
|
150
162
|
|
|
151
163
|
Keywords are the words that most set a memory apart from the others (TF-IDF). Words that should
|
|
152
|
-
never be keywords, such as your own name, go in `keyword_stopwords` in
|
|
164
|
+
never be keywords, such as your own name, go in Settings, which keeps them as `keyword_stopwords` in
|
|
153
165
|
`~/.config/agent-memory/config.json`.
|
|
154
166
|
|
|
155
167
|
The server only listens on loopback, and every request needs the one-time token from the link it
|
|
@@ -164,6 +176,17 @@ uv run ruff check .
|
|
|
164
176
|
uv run ruff format .
|
|
165
177
|
```
|
|
166
178
|
|
|
179
|
+
The page `mem ui` serves lives in `web/` (Svelte 5, TypeScript, Vite, Tailwind and bits-ui) and
|
|
180
|
+
needs Node 20 or newer. `npm run build` there writes it into `src/agent_memory/webui/`, which the
|
|
181
|
+
package ships; CI builds it before packaging, so the built files are not committed. While working on it, `npm run dev` serves the page with hot reload and passes API
|
|
182
|
+
calls to a running `mem ui` (set `MEM_UI` if it is not on port 8790); open it with the `#t=…` part of
|
|
183
|
+
the link `mem ui` printed.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cd web && npm ci && npm run build # the page, into the package
|
|
187
|
+
npm run check # svelte-check, warnings fail it
|
|
188
|
+
```
|
|
189
|
+
|
|
167
190
|
`uv run mem ...` runs the working copy without touching the installed one. To have every
|
|
168
191
|
session use the working copy, install it in place of the release and rewire the hooks; go back
|
|
169
192
|
the same way:
|
|
@@ -68,7 +68,8 @@ mem install
|
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
To upgrade, run `uv tool upgrade agent-memory-git` and then `mem install`, which refreshes the
|
|
71
|
-
protocol copies; `mem doctor` says when they are out of date.
|
|
71
|
+
protocol copies; `mem doctor` says when they are out of date. `mem ui` does both from Settings, with
|
|
72
|
+
one click, when a newer release is out.
|
|
72
73
|
|
|
73
74
|
`install` edits, and records in `~/.config/agent-memory/install-state.json`:
|
|
74
75
|
|
|
@@ -105,22 +106,33 @@ mem doctor check the installation
|
|
|
105
106
|
|
|
106
107
|
## Managing memories in a page
|
|
107
108
|
|
|
108
|
-
`mem ui` starts a small server on `127.0.0.1` and opens a page
|
|
109
|
-
terminal open while you use it. It
|
|
109
|
+
`mem ui` starts a small server on `127.0.0.1` and opens a page with three parts: Memories, Insights
|
|
110
|
+
and Settings (the logo always leads back to Memories). Keep the terminal open while you use it. It
|
|
111
|
+
needs `numpy`; the hooks do not.
|
|
110
112
|
|
|
111
113
|
- **Find.** One search box (press `/`). Word matches appear as you type, with the matching words
|
|
112
114
|
marked; a moment later the local model adds memories close in meaning, across Chinese and
|
|
113
115
|
English. Filters narrow the list by type, where a memory loads (everywhere, or one project or
|
|
114
116
|
folder), which tool wrote it, keyword, when it was added (a recent period or a date range), and
|
|
115
117
|
status (needs tidying, as `mem lint` reports and the open memory lists, or unchanged for 60
|
|
116
|
-
days). Tick several values in one filter to see memories that match any of them. The
|
|
117
|
-
|
|
118
|
-
- **Read.** Click a
|
|
119
|
-
the body rendered as Markdown
|
|
120
|
-
|
|
118
|
+
days). Tick several values in one filter to see memories that match any of them. The list's
|
|
119
|
+
header sets the order: newest, oldest, recently changed or A to Z.
|
|
120
|
+
- **Read.** Click a memory, or move with `j`/`k`. It opens beside the list (drag the line between
|
|
121
|
+
them to resize) with the body rendered as Markdown, `[[links]]` that open other memories, where
|
|
122
|
+
it came from, every other frontmatter field, the most similar memories, and the file exactly as
|
|
123
|
+
stored. On a narrow window the list and the memory take turns.
|
|
121
124
|
- **Change.** Edit (`e`) or delete from the open memory, add one with New (`n`), or tick several
|
|
122
|
-
|
|
123
|
-
with Undo. If an agent edits the same file meanwhile, saving is refused rather than
|
|
125
|
+
memories (`x`) and delete them together. Every change is one git commit, pushed in the
|
|
126
|
+
background, with Undo. If an agent edits the same file meanwhile, saving is refused rather than
|
|
127
|
+
overwriting.
|
|
128
|
+
- **Insights.** How many memories there are and how they change week by week, where they load and
|
|
129
|
+
which tool wrote them, pairs that read alike (compare them side by side and keep one), and the
|
|
130
|
+
index a session would get at a chosen place, against the 10,000-character hook limit.
|
|
131
|
+
- **Settings.** Light or dark, updates, the data repository and a sync on demand, words never to
|
|
132
|
+
use as keywords, the embedding model, and the same checks as `mem doctor`. Once a day the page
|
|
133
|
+
asks PyPI whether a newer release is out (only the package name is sent; turn it off there); when
|
|
134
|
+
`uv tool` installed agent-memory, Update runs the upgrade and `mem install` and restarts the page
|
|
135
|
+
on the new version.
|
|
124
136
|
|
|
125
137
|
Similar memories and search by meaning use Qwen3-Embedding-0.6B, int8 ONNX, on the CPU, in a
|
|
126
138
|
separate `uv run --with onnxruntime --with tokenizers` environment; vectors are cached by content
|
|
@@ -129,7 +141,7 @@ pinned revision, checked by SHA-256). Without it, or with `--keywords-only`, sim
|
|
|
129
141
|
come from keyword vectors and search matches words only.
|
|
130
142
|
|
|
131
143
|
Keywords are the words that most set a memory apart from the others (TF-IDF). Words that should
|
|
132
|
-
never be keywords, such as your own name, go in `keyword_stopwords` in
|
|
144
|
+
never be keywords, such as your own name, go in Settings, which keeps them as `keyword_stopwords` in
|
|
133
145
|
`~/.config/agent-memory/config.json`.
|
|
134
146
|
|
|
135
147
|
The server only listens on loopback, and every request needs the one-time token from the link it
|
|
@@ -144,6 +156,17 @@ uv run ruff check .
|
|
|
144
156
|
uv run ruff format .
|
|
145
157
|
```
|
|
146
158
|
|
|
159
|
+
The page `mem ui` serves lives in `web/` (Svelte 5, TypeScript, Vite, Tailwind and bits-ui) and
|
|
160
|
+
needs Node 20 or newer. `npm run build` there writes it into `src/agent_memory/webui/`, which the
|
|
161
|
+
package ships; CI builds it before packaging, so the built files are not committed. While working on it, `npm run dev` serves the page with hot reload and passes API
|
|
162
|
+
calls to a running `mem ui` (set `MEM_UI` if it is not on port 8790); open it with the `#t=…` part of
|
|
163
|
+
the link `mem ui` printed.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd web && npm ci && npm run build # the page, into the package
|
|
167
|
+
npm run check # svelte-check, warnings fail it
|
|
168
|
+
```
|
|
169
|
+
|
|
147
170
|
`uv run mem ...` runs the working copy without touching the installed one. To have every
|
|
148
171
|
session use the working copy, install it in place of the release and rewire the hooks; go back
|
|
149
172
|
the same way:
|
|
@@ -14,9 +14,21 @@ CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
|
14
14
|
PROTOCOL_COPY = CONFIG_DIR / "PROTOCOL.md" # what Claude Code imports; `mem install` refreshes it
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _python():
|
|
18
|
+
"""This interpreter, as the `python` beside it when that is the same file. Whether `mem` was
|
|
19
|
+
started as python3 or python3.11 (uv has changed its entry scripts) then leaves the hook
|
|
20
|
+
commands alone, so `mem install` does not rewrite them and Codex does not ask to trust them again."""
|
|
21
|
+
exe = Path(sys.executable)
|
|
22
|
+
try:
|
|
23
|
+
plain = exe.with_name("python")
|
|
24
|
+
return str(plain) if plain != exe and plain.samefile(exe) else sys.executable
|
|
25
|
+
except (OSError, ValueError): # no `python` beside it, or no sys.executable at all
|
|
26
|
+
return sys.executable
|
|
27
|
+
|
|
28
|
+
|
|
17
29
|
def mem_command():
|
|
18
30
|
"""The argv that runs `mem` with this interpreter, for hooks and background sync."""
|
|
19
|
-
return [
|
|
31
|
+
return [_python(), "-m", "agent_memory"]
|
|
20
32
|
|
|
21
33
|
|
|
22
34
|
def _mem_path():
|
|
@@ -479,13 +479,12 @@ def recent_hook_errors(log_text, now=None, days=ERROR_DAYS):
|
|
|
479
479
|
return out
|
|
480
480
|
|
|
481
481
|
|
|
482
|
-
def
|
|
483
|
-
|
|
482
|
+
def doctor_checks():
|
|
483
|
+
"""Every check as (passed, description), for `mem doctor` and the page's Health section."""
|
|
484
|
+
results = []
|
|
484
485
|
|
|
485
486
|
def check(cond, msg):
|
|
486
|
-
|
|
487
|
-
print(("ok " if cond else "FAIL ") + msg)
|
|
488
|
-
ok &= bool(cond)
|
|
487
|
+
results.append((bool(cond), msg))
|
|
489
488
|
|
|
490
489
|
data = config.data_dir()
|
|
491
490
|
check(data and (data / ".git").exists(), f"data repository: {data}")
|
|
@@ -528,4 +527,11 @@ def doctor():
|
|
|
528
527
|
errors = recent_hook_errors(_read(logf) or "")
|
|
529
528
|
latest = f"; latest: {errors[-1]} (details in {logf})" if errors else ""
|
|
530
529
|
check(not errors, f"no hook errors in the last {ERROR_DAYS} days ({len(errors)}{latest})")
|
|
531
|
-
return
|
|
530
|
+
return results
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def doctor():
|
|
534
|
+
results = doctor_checks()
|
|
535
|
+
for passed, msg in results:
|
|
536
|
+
print(("ok " if passed else "FAIL ") + msg)
|
|
537
|
+
return 0 if all(passed for passed, _ in results) else 1
|
|
@@ -10,7 +10,10 @@ background like hook commits; Undo reverts that commit.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import hashlib
|
|
13
|
+
import importlib.metadata
|
|
13
14
|
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
14
17
|
import secrets
|
|
15
18
|
import subprocess
|
|
16
19
|
import sys
|
|
@@ -21,18 +24,35 @@ from urllib.parse import parse_qs, urlparse
|
|
|
21
24
|
|
|
22
25
|
import numpy as np
|
|
23
26
|
|
|
24
|
-
from . import frontmatter, gitsync, store, ui
|
|
25
|
-
from .context import detect, hostname
|
|
27
|
+
from . import config, frontmatter, gitsync, select, store, ui, update
|
|
28
|
+
from .context import detect, hostname, normalize_remote
|
|
26
29
|
from .util import SCOPES, TYPES, atomic_write, id_path, log_exc, new_id, now_iso, slugify
|
|
27
30
|
|
|
28
|
-
STATIC = Path(__file__).with_name("webui")
|
|
29
|
-
MIME = {
|
|
31
|
+
STATIC = Path(__file__).with_name("webui") # the page, built from web/ by `npm run build`
|
|
32
|
+
MIME = {
|
|
33
|
+
".html": "text/html; charset=utf-8",
|
|
34
|
+
".css": "text/css; charset=utf-8",
|
|
35
|
+
".js": "text/javascript; charset=utf-8",
|
|
36
|
+
".svg": "image/svg+xml",
|
|
37
|
+
".png": "image/png",
|
|
38
|
+
".woff2": "font/woff2",
|
|
39
|
+
}
|
|
30
40
|
CSP = (
|
|
31
41
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; "
|
|
32
42
|
"connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
|
|
33
43
|
)
|
|
44
|
+
NOT_BUILT = (
|
|
45
|
+
"<!doctype html><meta charset=utf-8><title>agent-memory</title>"
|
|
46
|
+
"<p style='font:15px/1.6 system-ui;margin:3rem auto;max-width:36rem'>This copy of agent-memory has no built page. "
|
|
47
|
+
"In a checkout, build it with <code>cd web && npm ci && npm run build</code>, then reload.</p>"
|
|
48
|
+
)
|
|
49
|
+
RESTART_TOKEN = "AGENT_MEMORY_UI_TOKEN"
|
|
34
50
|
PLACE_FIELDS = ("cwd", "git_root", "git_remote", "git_branch")
|
|
35
51
|
SEARCH_HITS = 8
|
|
52
|
+
ACTIVITY_COMMITS = 2000
|
|
53
|
+
HOOK_LIMIT = 10000 # what Claude Code injects at most from a hook
|
|
54
|
+
COUNTS = re.compile(r"memory: \+(\d+) ~(\d+) -(\d+)")
|
|
55
|
+
UI_SUBJECT = re.compile(r"memory: (add|edit|delete|undo) (?:(\d+) memories)?")
|
|
36
56
|
|
|
37
57
|
|
|
38
58
|
class ApiError(Exception):
|
|
@@ -57,11 +77,13 @@ class UiApp:
|
|
|
57
77
|
|
|
58
78
|
def stamp(self):
|
|
59
79
|
head = gitsync.git(self.data, "rev-parse", "--short", "HEAD", timeout=5).stdout.strip()
|
|
80
|
+
# a push in the background moves the upstream ref but not HEAD; without it a stale "not pushed" would stay
|
|
81
|
+
pushed = gitsync.git(self.data, "rev-parse", "--short", "@{u}", timeout=5).stdout.strip()
|
|
60
82
|
newest, count = 0, 0
|
|
61
83
|
for path in self.mdir.rglob("*.md"):
|
|
62
84
|
count += 1
|
|
63
85
|
newest = max(newest, path.stat().st_mtime_ns)
|
|
64
|
-
return f"{head}:{count}:{newest}"
|
|
86
|
+
return f"{head}:{pushed}:{count}:{newest}"
|
|
65
87
|
|
|
66
88
|
def refresh(self):
|
|
67
89
|
with self.lock:
|
|
@@ -84,6 +106,116 @@ class UiApp:
|
|
|
84
106
|
ahead = int(r.stdout.strip()) if r.returncode == 0 and r.stdout.strip().isdigit() else 0
|
|
85
107
|
return {"remote": True, "ahead": ahead}
|
|
86
108
|
|
|
109
|
+
# ------------------------------------------------------------ insights and settings
|
|
110
|
+
|
|
111
|
+
def activity(self):
|
|
112
|
+
"""Commits to the data repository, newest first: when, which tool, how many memories added, changed, deleted."""
|
|
113
|
+
fmt = "%H%x1f%aI%x1f%s%x1f%(trailers:key=Tool,valueonly,separator=%x2C)%x1e"
|
|
114
|
+
r = gitsync.git(self.data, "log", f"--format={fmt}", "-n", str(ACTIVITY_COMMITS), timeout=10)
|
|
115
|
+
commits = []
|
|
116
|
+
for record in r.stdout.split("\x1e") if r.returncode == 0 else []:
|
|
117
|
+
parts = record.strip("\n").split("\x1f")
|
|
118
|
+
if len(parts) != 4 or not parts[2].startswith("memory: "):
|
|
119
|
+
continue
|
|
120
|
+
sha, when, subject, tool = parts
|
|
121
|
+
counts = COUNTS.match(subject)
|
|
122
|
+
if counts:
|
|
123
|
+
added, changed, deleted = map(int, counts.groups())
|
|
124
|
+
else:
|
|
125
|
+
ui_change = UI_SUBJECT.match(subject)
|
|
126
|
+
verb, many = ui_change.groups() if ui_change else ("", None)
|
|
127
|
+
n = int(many) if many else 1
|
|
128
|
+
added, changed, deleted = (
|
|
129
|
+
(n if verb == "add" else 0),
|
|
130
|
+
(n if verb in ("edit", "undo") else 0),
|
|
131
|
+
(n if verb == "delete" else 0),
|
|
132
|
+
)
|
|
133
|
+
commits.append(
|
|
134
|
+
{
|
|
135
|
+
"sha": sha[:10],
|
|
136
|
+
"date": when,
|
|
137
|
+
"subject": subject,
|
|
138
|
+
"tool": tool.strip() or None,
|
|
139
|
+
"added": added,
|
|
140
|
+
"changed": changed,
|
|
141
|
+
"deleted": deleted,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
return {"commits": commits}
|
|
145
|
+
|
|
146
|
+
def index_preview(self, place):
|
|
147
|
+
"""The index a session gets at one of the known places, or with no place only the global part."""
|
|
148
|
+
ctx = {}
|
|
149
|
+
if place:
|
|
150
|
+
known = {p["key"]: p.get("ctx", {}) for p in self.current().get("places", [])}
|
|
151
|
+
if place not in known:
|
|
152
|
+
raise ApiError(404, "unknown place")
|
|
153
|
+
ctx = dict(known[place])
|
|
154
|
+
memories = store.load_all(self.data)
|
|
155
|
+
paths = {
|
|
156
|
+
"memories": str(self.mdir),
|
|
157
|
+
"staging": str(self.data / "staging" / "<session>"),
|
|
158
|
+
"mem": str(config.BIN),
|
|
159
|
+
}
|
|
160
|
+
text = select.render(memories, ctx, paths)
|
|
161
|
+
globals_, here = select.visible(memories, ctx)
|
|
162
|
+
return {"text": text, "chars": len(text), "limit": HOOK_LIMIT, "global": len(globals_), "here": len(here)}
|
|
163
|
+
|
|
164
|
+
def settings(self):
|
|
165
|
+
remote = gitsync.git(self.data, "remote", "get-url", "origin", timeout=5)
|
|
166
|
+
try:
|
|
167
|
+
version = importlib.metadata.version("agent-memory-git")
|
|
168
|
+
except importlib.metadata.PackageNotFoundError:
|
|
169
|
+
version = "dev"
|
|
170
|
+
return {
|
|
171
|
+
"version": version,
|
|
172
|
+
"dataDir": ui._short(str(self.data)),
|
|
173
|
+
"remote": normalize_remote(remote.stdout.strip()) if remote.returncode == 0 else None,
|
|
174
|
+
"sync": self.sync_status(),
|
|
175
|
+
"keywordStopwords": config.load().get("keyword_stopwords", []),
|
|
176
|
+
"model": {
|
|
177
|
+
"name": ui.MODEL["name"],
|
|
178
|
+
"ready": ui.model_ready(),
|
|
179
|
+
"dir": ui._short(str(ui.model_dir())),
|
|
180
|
+
"fetch": "mem ui --fetch-model",
|
|
181
|
+
"size": "about 625 MB",
|
|
182
|
+
},
|
|
183
|
+
"semantic": bool(self.state.get("semantic")),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def set_settings(self, p):
|
|
187
|
+
"""Change keyword_stopwords, check_updates, or both."""
|
|
188
|
+
cfg = config.load()
|
|
189
|
+
if "keyword_stopwords" in p:
|
|
190
|
+
words = p["keyword_stopwords"]
|
|
191
|
+
if not isinstance(words, list) or not all(isinstance(w, str) for w in words):
|
|
192
|
+
raise ApiError(400, "keyword_stopwords must be a list of words")
|
|
193
|
+
cfg["keyword_stopwords"] = sorted({w.strip().lower() for w in words if w.strip()})
|
|
194
|
+
if "check_updates" in p:
|
|
195
|
+
if not isinstance(p["check_updates"], bool):
|
|
196
|
+
raise ApiError(400, "check_updates must be true or false")
|
|
197
|
+
cfg["check_updates"] = p["check_updates"]
|
|
198
|
+
config.save(cfg)
|
|
199
|
+
if "keyword_stopwords" in p:
|
|
200
|
+
self.refresh() # keywords are chosen when the state is built
|
|
201
|
+
return self.settings()
|
|
202
|
+
|
|
203
|
+
def check_update(self, p):
|
|
204
|
+
return update.status(force=True)
|
|
205
|
+
|
|
206
|
+
def run_update(self, p):
|
|
207
|
+
return update.upgrade()
|
|
208
|
+
|
|
209
|
+
def doctor(self):
|
|
210
|
+
from .install import doctor_checks
|
|
211
|
+
|
|
212
|
+
return {"checks": [{"ok": passed, "text": text} for passed, text in doctor_checks()]}
|
|
213
|
+
|
|
214
|
+
def sync(self, p):
|
|
215
|
+
ok = gitsync.sync(self.data)
|
|
216
|
+
self.refresh()
|
|
217
|
+
return {"ok": ok, "sync": self.state["sync"]}
|
|
218
|
+
|
|
87
219
|
def search(self, query):
|
|
88
220
|
"""Memories closest in meaning to the query (the page does word matching itself)."""
|
|
89
221
|
query = (query or "").strip()
|
|
@@ -300,7 +432,16 @@ class UiApp:
|
|
|
300
432
|
|
|
301
433
|
class Handler(BaseHTTPRequestHandler):
|
|
302
434
|
server_version = "agent-memory"
|
|
303
|
-
POSTS = {
|
|
435
|
+
POSTS = {
|
|
436
|
+
"/api/create": "create",
|
|
437
|
+
"/api/edit": "edit",
|
|
438
|
+
"/api/delete": "delete",
|
|
439
|
+
"/api/undo": "undo",
|
|
440
|
+
"/api/settings": "set_settings",
|
|
441
|
+
"/api/sync": "sync",
|
|
442
|
+
"/api/update/check": "check_update",
|
|
443
|
+
"/api/update": "run_update",
|
|
444
|
+
}
|
|
304
445
|
|
|
305
446
|
def log_message(self, *args):
|
|
306
447
|
pass
|
|
@@ -351,25 +492,44 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
351
492
|
if not self._guard(api):
|
|
352
493
|
return
|
|
353
494
|
if not api:
|
|
354
|
-
|
|
355
|
-
path = STATIC / name
|
|
356
|
-
if "/" in name or not path.is_file() or path.suffix not in MIME:
|
|
357
|
-
return self._send(404, {"error": "not found"})
|
|
358
|
-
return self._send(200, path.read_bytes(), MIME[path.suffix])
|
|
495
|
+
return self._page(url.path.lstrip("/"))
|
|
359
496
|
app, q = self.server.app, {k: v[0] for k, v in parse_qs(url.query).items()}
|
|
360
497
|
routes = {
|
|
361
498
|
"/api/state": app.current,
|
|
362
499
|
"/api/stamp": lambda: {"stamp": app.stamp()},
|
|
363
500
|
"/api/search": lambda: app.search(q.get("q")),
|
|
501
|
+
"/api/activity": app.activity,
|
|
502
|
+
"/api/index-preview": lambda: app.index_preview(q.get("place", "")),
|
|
503
|
+
"/api/settings": app.settings,
|
|
504
|
+
"/api/doctor": app.doctor,
|
|
505
|
+
"/api/update": update.status,
|
|
364
506
|
}
|
|
365
507
|
if url.path not in routes:
|
|
366
508
|
return self._send(404, {"error": "not found"})
|
|
367
509
|
self._run(routes[url.path])
|
|
368
510
|
|
|
511
|
+
def _page(self, rel):
|
|
512
|
+
"""The page's files, and index.html for any other path so the page can route."""
|
|
513
|
+
root = STATIC.resolve()
|
|
514
|
+
target = (root / rel).resolve() if rel else None
|
|
515
|
+
if target and target.is_file() and root in target.parents and target.suffix in MIME:
|
|
516
|
+
return self._send(200, target.read_bytes(), MIME[target.suffix])
|
|
517
|
+
index = root / "index.html"
|
|
518
|
+
if not index.is_file(): # a checkout where the page was never built
|
|
519
|
+
return self._send(503, NOT_BUILT.encode(), MIME[".html"])
|
|
520
|
+
if rel.startswith("assets/"):
|
|
521
|
+
return self._send(404, {"error": "not found"})
|
|
522
|
+
return self._send(200, index.read_bytes(), MIME[".html"])
|
|
523
|
+
|
|
369
524
|
def do_POST(self):
|
|
370
525
|
url = urlparse(self.path)
|
|
371
526
|
if not self._guard(True):
|
|
372
527
|
return
|
|
528
|
+
if url.path == "/api/restart": # after an update: answer, then serve_forever ends and run() starts the new code
|
|
529
|
+
self.server.restart = True
|
|
530
|
+
self._send(200, {"restarting": True})
|
|
531
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start()
|
|
532
|
+
return
|
|
373
533
|
method = self.POSTS.get(url.path)
|
|
374
534
|
if not method:
|
|
375
535
|
return self._send(404, {"error": "not found"})
|
|
@@ -389,7 +549,9 @@ def make_server(data_dir, port=8790, semantic=True):
|
|
|
389
549
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
390
550
|
port = server.server_address[1]
|
|
391
551
|
server.hosts = {f"127.0.0.1:{port}", f"localhost:{port}"}
|
|
392
|
-
|
|
552
|
+
# a restart after an update keeps the token, so the open page carries on
|
|
553
|
+
server.token = os.environ.pop(RESTART_TOKEN, None) or secrets.token_urlsafe(18)
|
|
554
|
+
server.restart = False
|
|
393
555
|
server.app = app
|
|
394
556
|
server.url = f"http://127.0.0.1:{port}/#t={server.token}"
|
|
395
557
|
return server
|
|
@@ -418,3 +580,8 @@ def run(data_dir, port=8790, open_browser=True, semantic=True):
|
|
|
418
580
|
server.server_close()
|
|
419
581
|
if server.app.encoder:
|
|
420
582
|
server.app.encoder.close()
|
|
583
|
+
if server.restart:
|
|
584
|
+
args = ["ui", "--port", str(server.server_address[1]), "--no-open"] + ([] if semantic else ["--keywords-only"])
|
|
585
|
+
print("Restarting with the updated version...", flush=True)
|
|
586
|
+
os.environ[RESTART_TOKEN] = server.token
|
|
587
|
+
os.execv(sys.executable, [sys.executable, "-m", "agent_memory", *args])
|