agent-memory-cli 0.1.0__py3-none-any.whl
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/__init__.py +9 -0
- agent_memory/__main__.py +9 -0
- agent_memory/archive.py +359 -0
- agent_memory/cli.py +437 -0
- agent_memory/debrief.py +336 -0
- agent_memory/doctor.py +710 -0
- agent_memory/git_runner.py +68 -0
- agent_memory/home.py +246 -0
- agent_memory/layout.py +198 -0
- agent_memory/org.py +218 -0
- agent_memory/publication.py +433 -0
- agent_memory/setup/__init__.py +53 -0
- agent_memory/setup/claude.py +34 -0
- agent_memory/setup/codex.py +39 -0
- agent_memory/setup/common.py +1015 -0
- agent_memory/setup/legacy.py +67 -0
- agent_memory/startup.py +254 -0
- agent_memory/status.py +137 -0
- agent_memory/sync.py +649 -0
- agent_memory/templates/org-memory/decisions.md +5 -0
- agent_memory/templates/org-memory/recent.md +16 -0
- agent_memory/templates/org-memory/rules.md +5 -0
- agent_memory/templates/project-memory/decision_log.md +3 -0
- agent_memory/templates/project-memory/known_debt.md +8 -0
- agent_memory/templates/project-memory/open_threads.md +3 -0
- agent_memory/templates/project-memory/project_facts.md +4 -0
- agent_memory/templates/workflow/SKILL.md +42 -0
- agent_memory/workflow.py +287 -0
- agent_memory_cli-0.1.0.dist-info/METADATA +261 -0
- agent_memory_cli-0.1.0.dist-info/RECORD +33 -0
- agent_memory_cli-0.1.0.dist-info/WHEEL +4 -0
- agent_memory_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agent_memory_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-memory
|
|
3
|
+
description: Cross-session memory for this repository. Use at session start to read the bounded memory context (recall), and whenever a decision is made that a later session must not rediscover (capture). Also use when asked what was decided before.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# agent-memory workflow for {runtime}
|
|
7
|
+
|
|
8
|
+
Managed by `agent-memory setup {runtime}`: a rerun regenerates this file only
|
|
9
|
+
while its digest matches a shipped template; an edited file is reported as a
|
|
10
|
+
conflict and kept. The rules are the same for every runtime; only the agent
|
|
11
|
+
name differs.
|
|
12
|
+
|
|
13
|
+
## Recall, at session start
|
|
14
|
+
|
|
15
|
+
The session-start hook printed the startup manifest: the memory files to read,
|
|
16
|
+
in order, with their readability only. No content was injected. Before acting
|
|
17
|
+
on project work, read the bounded context in one step:
|
|
18
|
+
|
|
19
|
+
agent-memory recall
|
|
20
|
+
|
|
21
|
+
It prints the project's active files (`project_facts.md`, `decision_log.md`,
|
|
22
|
+
`open_threads.md`, `known_debt.md`), then the curated org files (`recent.md`,
|
|
23
|
+
`decisions.md`, `rules.md`), cut at a character budget (`--max-chars`, default
|
|
24
|
+
8000). Cite a decision by its date heading and its text. `archive/`, `events/`
|
|
25
|
+
and `debriefs/` are never loaded; read them only when the work needs them.
|
|
26
|
+
|
|
27
|
+
## Capture, when a decision is made
|
|
28
|
+
|
|
29
|
+
When the user decides something that a future session must not rediscover,
|
|
30
|
+
record it once, in one sentence, with the reason:
|
|
31
|
+
|
|
32
|
+
agent-memory capture --agent {runtime} "<the decision>" --why "<the reason>" --source "<PR, issue or message>"
|
|
33
|
+
|
|
34
|
+
It appends a dated entry, newest first, to the project's `decision_log.md`,
|
|
35
|
+
with provenance: the agent, the UTC time and the source. Capture only what the
|
|
36
|
+
user decided, not what you propose; confirm first when in doubt. One decision
|
|
37
|
+
per entry. Stable facts go to `project_facts.md` by hand, not through capture.
|
|
38
|
+
|
|
39
|
+
## Out of scope
|
|
40
|
+
|
|
41
|
+
No synthesis, search or index; no writes to the org tier; no push. Syncing the
|
|
42
|
+
home is `agent-memory push`, run deliberately, never from this workflow.
|
agent_memory/workflow.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Kiloloop
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""The minimal memory workflow: ``agent-memory capture`` and ``agent-memory recall``.
|
|
4
|
+
|
|
5
|
+
Two verbs and one authored text. ``capture`` appends one decision, with its
|
|
6
|
+
provenance, to the project's ``decision_log.md``, newest first under a UTC
|
|
7
|
+
date heading; it never writes through a symlink (a linked component anywhere
|
|
8
|
+
below the home, the file included, is refused, never followed), replaces the
|
|
9
|
+
file atomically with the file's own mode, and touches no other file. ``recall`` prints the content of the files the
|
|
10
|
+
startup manifest lists, in the manifest's order, cut at a character budget
|
|
11
|
+
with a notice: the bounded read the manifest describes but never performs.
|
|
12
|
+
``archive/``, ``events/`` and ``debriefs/`` are never loaded.
|
|
13
|
+
|
|
14
|
+
The authored text is the workflow file ``setup <runtime>`` installs beside the
|
|
15
|
+
hook, rendered from one shipped template with the runtime's name; every
|
|
16
|
+
runtime's wrapper says the same thing.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import datetime as dt
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import secrets
|
|
25
|
+
import stat as stat_mod
|
|
26
|
+
from importlib import resources
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from . import layout, startup
|
|
31
|
+
|
|
32
|
+
SCHEMA_VERSION = 1
|
|
33
|
+
DECISION_FILE = "decision_log.md"
|
|
34
|
+
DEFAULT_AGENT_ENV = "AGENT_MEMORY_AGENT"
|
|
35
|
+
WORKFLOW_TEMPLATE = "templates/workflow/SKILL.md"
|
|
36
|
+
|
|
37
|
+
_HEADING = re.compile(r"^## (\d{4}-\d{2}-\d{2})\s*$")
|
|
38
|
+
_SPACE = re.compile(r"\s+")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WorkflowError(Exception):
|
|
42
|
+
"""A precondition failed; nothing was written."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# --- the authored text ---------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def workflow_text(runtime: str) -> str:
|
|
49
|
+
"""The workflow file for ``runtime``, byte for byte what setup writes."""
|
|
50
|
+
template = (resources.files(__package__) / WORKFLOW_TEMPLATE).read_text(encoding="utf-8")
|
|
51
|
+
return template.replace("{runtime}", runtime)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- capture -------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def default_agent() -> str:
|
|
58
|
+
"""Who is capturing: ``$AGENT_MEMORY_AGENT`` (the hook exports it), else the user, else ``unknown``."""
|
|
59
|
+
return os.environ.get(DEFAULT_AGENT_ENV) or os.environ.get("USER") or "unknown"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def default_runtime() -> str:
|
|
63
|
+
"""The runtime reading: ``$AGENT_MEMORY_AGENT`` when it names one, else claude."""
|
|
64
|
+
agent = os.environ.get(DEFAULT_AGENT_ENV)
|
|
65
|
+
return agent if agent in startup.RUNTIMES else startup.RUNTIME_CLAUDE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def format_entry(decision: str, *, why: Optional[str], agent: str, source: Optional[str], now: dt.datetime) -> str:
|
|
69
|
+
"""One decision as a list line: the decision, its reason, then the provenance in parentheses."""
|
|
70
|
+
text = _one_line(decision)
|
|
71
|
+
if not text:
|
|
72
|
+
raise WorkflowError("the decision is empty")
|
|
73
|
+
parts = [f"- **{text}**"]
|
|
74
|
+
reason = _one_line(why or "")
|
|
75
|
+
if reason:
|
|
76
|
+
parts.append(f" Why: {reason}")
|
|
77
|
+
provenance = [_one_line(agent) or "unknown", _iso(now)]
|
|
78
|
+
reference = _one_line(source or "")
|
|
79
|
+
if reference:
|
|
80
|
+
provenance.append(f"source: {reference}")
|
|
81
|
+
parts.append(f" ({', '.join(provenance)})")
|
|
82
|
+
return "".join(parts)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def capture(
|
|
86
|
+
home: Path,
|
|
87
|
+
project: str,
|
|
88
|
+
decision: str,
|
|
89
|
+
*,
|
|
90
|
+
why: Optional[str] = None,
|
|
91
|
+
source: Optional[str] = None,
|
|
92
|
+
agent: Optional[str] = None,
|
|
93
|
+
now: Optional[dt.datetime] = None,
|
|
94
|
+
dry_run: bool = False,
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
"""Append ``decision`` to the project's decision log, newest first under today's UTC heading."""
|
|
97
|
+
home = Path(home).expanduser().absolute()
|
|
98
|
+
layout.validate_project_name(project)
|
|
99
|
+
memory = layout.project_memory_dir(home, project)
|
|
100
|
+
path = memory / DECISION_FILE
|
|
101
|
+
moment = now or dt.datetime.now(dt.timezone.utc)
|
|
102
|
+
agent_name = agent or default_agent()
|
|
103
|
+
entry = format_entry(decision, why=why, agent=agent_name, source=source, now=moment)
|
|
104
|
+
date = _iso(moment)[:10]
|
|
105
|
+
|
|
106
|
+
link = _linked_component(home, path)
|
|
107
|
+
if link is not None:
|
|
108
|
+
raise WorkflowError(f"{link} is a symlink; the workflow never writes through a link")
|
|
109
|
+
if not memory.is_dir():
|
|
110
|
+
raise WorkflowError(
|
|
111
|
+
f"project {project!r} has no memory tier at {memory}; create it with `agent-memory init --project {project}`"
|
|
112
|
+
)
|
|
113
|
+
try:
|
|
114
|
+
info = os.lstat(path)
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
raise WorkflowError(f"{path} is missing; recreate it with `agent-memory init --project {project}`") from None
|
|
117
|
+
except OSError as exc:
|
|
118
|
+
raise WorkflowError(f"cannot inspect {path}: {exc}") from exc
|
|
119
|
+
if not stat_mod.S_ISREG(info.st_mode):
|
|
120
|
+
raise WorkflowError(f"{path} is not a regular file")
|
|
121
|
+
try:
|
|
122
|
+
before = path.read_bytes().decode("utf-8")
|
|
123
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
124
|
+
raise WorkflowError(f"cannot read {path}: {exc}") from exc
|
|
125
|
+
|
|
126
|
+
after = insert_entry(before, date, entry)
|
|
127
|
+
result = {
|
|
128
|
+
"schema_version": SCHEMA_VERSION,
|
|
129
|
+
"action": "capture",
|
|
130
|
+
"home": str(home),
|
|
131
|
+
"project": project,
|
|
132
|
+
"path": str(path),
|
|
133
|
+
"date": date,
|
|
134
|
+
"entry": entry,
|
|
135
|
+
"agent": agent_name,
|
|
136
|
+
"dry_run": dry_run,
|
|
137
|
+
"written": False,
|
|
138
|
+
}
|
|
139
|
+
if dry_run:
|
|
140
|
+
return result
|
|
141
|
+
_replace_text(path, after, mode=stat_mod.S_IMODE(info.st_mode))
|
|
142
|
+
result["written"] = True
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def insert_entry(text: str, date: str, entry: str) -> str:
|
|
147
|
+
"""``text`` with ``entry`` as the first item under the ``## date`` heading, creating the heading newest-first."""
|
|
148
|
+
lines = text.split("\n")
|
|
149
|
+
if lines and lines[-1] == "":
|
|
150
|
+
lines.pop() # the trailing newline; restored below
|
|
151
|
+
first = next((index for index, line in enumerate(lines) if _HEADING.match(line)), None)
|
|
152
|
+
if first is not None and _HEADING.match(lines[first]).group(1) == date:
|
|
153
|
+
at = first + 1
|
|
154
|
+
if at < len(lines) and lines[at] == "":
|
|
155
|
+
at += 1
|
|
156
|
+
lines[at:at] = [entry]
|
|
157
|
+
else:
|
|
158
|
+
block = [f"## {date}", "", entry, ""]
|
|
159
|
+
if first is None:
|
|
160
|
+
if lines and lines[-1] != "":
|
|
161
|
+
lines.append("")
|
|
162
|
+
lines.extend(block[:-1])
|
|
163
|
+
else:
|
|
164
|
+
at = first
|
|
165
|
+
if at > 0 and lines[at - 1] != "":
|
|
166
|
+
block.insert(0, "")
|
|
167
|
+
lines[at:at] = block
|
|
168
|
+
return "\n".join(lines) + "\n"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _linked_component(home: Path, path: Path) -> Optional[Path]:
|
|
172
|
+
"""The first symlink below ``home`` on the way down to ``path`` (``path`` included), or ``None``.
|
|
173
|
+
|
|
174
|
+
The home itself may be a link (a workspace marker is one); every component
|
|
175
|
+
beneath it is inspected without following, so a linked ``projects/``,
|
|
176
|
+
project or memory directory is refused before anything is read or staged.
|
|
177
|
+
A component that does not exist ends the walk: the later checks name it.
|
|
178
|
+
"""
|
|
179
|
+
current = home
|
|
180
|
+
for part in path.relative_to(home).parts:
|
|
181
|
+
current = current / part
|
|
182
|
+
try:
|
|
183
|
+
info = os.lstat(current)
|
|
184
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
185
|
+
return None
|
|
186
|
+
except OSError as exc:
|
|
187
|
+
raise WorkflowError(f"cannot inspect {current}: {exc}") from exc
|
|
188
|
+
if stat_mod.S_ISLNK(info.st_mode):
|
|
189
|
+
return current
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _replace_text(path: Path, text: str, *, mode: int) -> None:
|
|
194
|
+
"""Write ``text`` beside ``path`` and move it into place atomically; the file keeps its mode."""
|
|
195
|
+
data = text.encode("utf-8")
|
|
196
|
+
temp = path.with_name(f".{path.name}.{secrets.token_hex(4)}.tmp")
|
|
197
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
|
198
|
+
try:
|
|
199
|
+
fd = os.open(temp, flags, mode)
|
|
200
|
+
except OSError as exc:
|
|
201
|
+
raise WorkflowError(f"cannot stage the write beside {path}: {exc}") from exc
|
|
202
|
+
try:
|
|
203
|
+
with os.fdopen(fd, "wb") as handle:
|
|
204
|
+
os.fchmod(handle.fileno(), mode) # os.open applied the umask; the original bits win
|
|
205
|
+
handle.write(data)
|
|
206
|
+
handle.flush()
|
|
207
|
+
os.fsync(handle.fileno())
|
|
208
|
+
os.replace(temp, path)
|
|
209
|
+
except OSError as exc:
|
|
210
|
+
try:
|
|
211
|
+
os.unlink(temp)
|
|
212
|
+
except OSError:
|
|
213
|
+
pass
|
|
214
|
+
raise WorkflowError(f"cannot write {path}: {exc}") from exc
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# --- recall --------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def recall(
|
|
221
|
+
home: Path,
|
|
222
|
+
*,
|
|
223
|
+
project: Optional[str],
|
|
224
|
+
runtime: str = startup.RUNTIME_CLAUDE,
|
|
225
|
+
max_chars: int = startup.DEFAULT_MAX_CHARS,
|
|
226
|
+
home_source: str = "flag",
|
|
227
|
+
project_source: Optional[str] = None,
|
|
228
|
+
notes: List[str] = (),
|
|
229
|
+
) -> Dict[str, Any]:
|
|
230
|
+
"""The bounded read: the manifest's readable files, in its order, with their content, cut at ``max_chars``."""
|
|
231
|
+
manifest = startup.build_manifest(
|
|
232
|
+
home, runtime=runtime, project=project, home_source=home_source, project_source=project_source, notes=notes
|
|
233
|
+
)
|
|
234
|
+
parts: List[str] = []
|
|
235
|
+
files: List[Dict[str, Any]] = []
|
|
236
|
+
head = f"agent-memory recall: home {manifest['home']}"
|
|
237
|
+
head += f", project {manifest['project']}" if manifest["project"] is not None else ", no project"
|
|
238
|
+
parts.append(head + f"; content follows in manifest order, cut at {max_chars} characters.")
|
|
239
|
+
for entry in manifest["files"]:
|
|
240
|
+
row = {"tier": entry["tier"], "relative": entry["relative"], "state": entry["state"], "bytes": entry["bytes"]}
|
|
241
|
+
if entry["state"] == startup.READABLE:
|
|
242
|
+
try:
|
|
243
|
+
content = Path(entry["path"]).read_bytes().decode("utf-8", errors="replace")
|
|
244
|
+
except OSError as exc:
|
|
245
|
+
row["state"] = startup.UNREADABLE
|
|
246
|
+
row["error"] = exc.strerror or str(exc)
|
|
247
|
+
manifest["warnings"].append(f"{entry['relative']}: unreadable ({row['error']})")
|
|
248
|
+
else:
|
|
249
|
+
parts.append(f"\n--- {entry['relative']} ({entry['bytes']} bytes, modified {entry['modified_at_utc']}) ---")
|
|
250
|
+
parts.append(content.rstrip("\n"))
|
|
251
|
+
files.append(row)
|
|
252
|
+
parts.append(f"\nExcluded by default: {', '.join(manifest['excluded'])}")
|
|
253
|
+
if manifest["warnings"]:
|
|
254
|
+
parts.append("Warnings:")
|
|
255
|
+
parts.extend(f" - {warning}" for warning in manifest["warnings"])
|
|
256
|
+
text = _bound("\n".join(parts) + "\n", max_chars)
|
|
257
|
+
return {
|
|
258
|
+
"schema_version": SCHEMA_VERSION,
|
|
259
|
+
"action": "recall",
|
|
260
|
+
"home": manifest["home"],
|
|
261
|
+
"project": manifest["project"],
|
|
262
|
+
"content_injected": True,
|
|
263
|
+
"max_chars": max_chars,
|
|
264
|
+
"files": files,
|
|
265
|
+
"excluded": manifest["excluded"],
|
|
266
|
+
"warnings": manifest["warnings"],
|
|
267
|
+
"truncated": len(text) < len("\n".join(parts)) + 1,
|
|
268
|
+
"text": text,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _bound(text: str, max_chars: int) -> str:
|
|
273
|
+
max_chars = max(0, max_chars)
|
|
274
|
+
if len(text) <= max_chars:
|
|
275
|
+
return text
|
|
276
|
+
suffix = f"\n[agent-memory: recall cut at {max_chars} characters; raise --max-chars or read the remaining files directly]\n"
|
|
277
|
+
if len(suffix) >= max_chars:
|
|
278
|
+
suffix = "[cut]\n"[:max_chars]
|
|
279
|
+
return text[: max_chars - len(suffix)] + suffix
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _one_line(text: str) -> str:
|
|
283
|
+
return _SPACE.sub(" ", text).strip()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _iso(moment: dt.datetime) -> str:
|
|
287
|
+
return moment.astimezone(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-memory-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cross-session memory for coding agents: plain files, git-native, no server
|
|
5
|
+
Project-URL: Homepage, https://github.com/kiloloop/agent-memory
|
|
6
|
+
Project-URL: Source, https://github.com/kiloloop/agent-memory
|
|
7
|
+
Project-URL: Changelog, https://github.com/kiloloop/agent-memory/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Kiloloop
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,git,markdown,memory
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest<10,>=8; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff<1,>=0.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: twine<8,>=7; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# agent-memory
|
|
32
|
+
|
|
33
|
+
Cross-session memory for coding agents — plain files, git-native, no server.
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
Agents start sessions blank. Keep facts, decisions, and unfinished work in
|
|
38
|
+
Markdown you can read and edit.
|
|
39
|
+
|
|
40
|
+
Memory travels **between agents** via project files, **between projects** via
|
|
41
|
+
org rules, and **between machines** via optional git sync. Claude Code and Codex
|
|
42
|
+
share the store. No database, daemon, server, or runtime Python dependencies.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
First time: install and bind. Every session after: recall context, capture decisions.
|
|
47
|
+
|
|
48
|
+
### With your coding agent
|
|
49
|
+
|
|
50
|
+
Paste this into Claude Code or Codex from your repository:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
Set up cross-session memory with agent-memory-cli from
|
|
54
|
+
https://pypi.org/project/agent-memory-cli/ and its source,
|
|
55
|
+
https://github.com/kiloloop/agent-memory.
|
|
56
|
+
|
|
57
|
+
Install with Python 3.10+. Ask for my home and project name, initialize and
|
|
58
|
+
bind this repository, and git-ignore the binding. Set up my runtime's hook
|
|
59
|
+
and workflow; report conflicts.
|
|
60
|
+
|
|
61
|
+
Capture a decision I provide, with its reason and runtime. Show status and
|
|
62
|
+
doctor. Next session here, recall context and report any truncation or sync
|
|
63
|
+
warning. Ask for my remote before enabling optional sync; keep pushing explicit.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Manually
|
|
67
|
+
|
|
68
|
+
Python 3.10+ is required; sync needs git 2.25+, hooks use Bash, and
|
|
69
|
+
`archive`/`restore` are POSIX-only.
|
|
70
|
+
|
|
71
|
+
1. **Install** with `uv tool install agent-memory-cli` or
|
|
72
|
+
`python -m pip install agent-memory-cli`. The distribution and the command
|
|
73
|
+
differ: installing `agent-memory-cli` gives you `agent-memory`. Put it on
|
|
74
|
+
your agent's PATH.
|
|
75
|
+
2. **Bind**, from your repository root:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
agent-memory init --home "$HOME/agent-memory" --project my-app --repo .
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Git-ignore `.agent-memory.json`. Stay here; unset `AGENT_MEMORY_HOME`/`OACP_HOME` or point them at this
|
|
82
|
+
home: they precede the binding.
|
|
83
|
+
3. **Set up** the runtime you use, then enable the hook there if required:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
agent-memory setup claude
|
|
87
|
+
# Or:
|
|
88
|
+
agent-memory setup codex
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
4. **Record now; recall next session** in this repository:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
agent-memory capture "Use SQLite for the cache." --why "no daemon" --agent codex
|
|
95
|
+
agent-memory recall
|
|
96
|
+
agent-memory status
|
|
97
|
+
agent-memory doctor
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use `--agent claude` if appropriate; recall also works now for inspection.
|
|
101
|
+
5. **Optionally sync.** Use an empty private remote. `enable --remote` pushes
|
|
102
|
+
the initial commit; substitute its URL:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
agent-memory enable --remote git@github.com:YOUR_ORG/agent-memory-store.git
|
|
106
|
+
agent-memory push --agent codex
|
|
107
|
+
# After another machine pushes, with a clean local tree:
|
|
108
|
+
agent-memory pull
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Elsewhere, `agent-memory clone <remote-url> --home <local-home>`, then repeat
|
|
112
|
+
binding and setup. Bind each machine separately; push explicitly.
|
|
113
|
+
|
|
114
|
+
## How It Works
|
|
115
|
+
|
|
116
|
+

|
|
117
|
+
|
|
118
|
+
*Illustrative files; not a bundled application UI.*
|
|
119
|
+
|
|
120
|
+
OACP defines the layout; this tool implements it. The four active files in
|
|
121
|
+
`projects/<name>/memory/` hold notes such as these trimmed samples:
|
|
122
|
+
|
|
123
|
+
| File | Purpose and sample |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `project_facts.md` | Stable facts: FastAPI backend; Postgres; no PII in logs. |
|
|
126
|
+
| `decision_log.md` | Choices: 2026-05-09 — retry 5xx three times, with backoff and jitter. |
|
|
127
|
+
| `open_threads.md` | Work and owners: OAuth refresh race — waiting on Codex. |
|
|
128
|
+
| `known_debt.md` | Problems: replace the hard-coded session TTL with a setting. |
|
|
129
|
+
|
|
130
|
+
Date entries; supersede decisions by adding new ones. Close or pause threads,
|
|
131
|
+
write for humans, and distill transcripts. Edit other files directly; promote
|
|
132
|
+
debt to a thread when work starts.
|
|
133
|
+
|
|
134
|
+
`org-memory/` sits beside `projects/`: `recent.md`, `decisions.md`, and
|
|
135
|
+
`rules.md` carry shared context; `events/` and `debriefs/` hold records.
|
|
136
|
+
Most notes belong to a project. Use org memory only across repositories.
|
|
137
|
+
|
|
138
|
+
Home resolution: `--home` → `AGENT_MEMORY_HOME` → `OACP_HOME` → nearest ancestor
|
|
139
|
+
`.agent-memory.json` → workspace marker → `~/agent-memory`. A workspace marker
|
|
140
|
+
points into a home's `projects/` tree. Project selection uses `--project` or a
|
|
141
|
+
matching binding/marker. OACP is not required.
|
|
142
|
+
|
|
143
|
+
Sync makes the home a git repository with an allowlist and `.oacp-memory-repo`
|
|
144
|
+
marker. `push` commits selected paths; `pull` fast-forwards a clean tree that
|
|
145
|
+
is not ahead or diverged. No merges; keys and setup receipts stay local.
|
|
146
|
+
Network verbs time out after 30 seconds.
|
|
147
|
+
|
|
148
|
+
`setup` installs a SessionStart hook and memory workflow. `startup` lists
|
|
149
|
+
metadata for the four project files, then the three curated org files; it
|
|
150
|
+
injects no content. `--pull` refreshes first, warning on failure. The workflow
|
|
151
|
+
tells the agent to run `recall` for an 8,000-character bounded read. Raise
|
|
152
|
+
`--max-chars` or read remaining files directly when cut. `capture` records
|
|
153
|
+
decisions during work. At the end, update threads and debt, optionally publish
|
|
154
|
+
a summary with `debrief write`, and explicitly `push`; no push hook is installed.
|
|
155
|
+
|
|
156
|
+
Startup and recall exclude `archive/`, `events/`, and `debriefs/`. This is not
|
|
157
|
+
a vector database, RAG pipeline, or chat-history store: no embeddings,
|
|
158
|
+
similarity queries, synthesis, or indexing. Recall reads a fixed file set.
|
|
159
|
+
|
|
160
|
+
### Commands
|
|
161
|
+
|
|
162
|
+
| Command | Purpose |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| [`status`][status] | Inspect home and sync. |
|
|
165
|
+
| [`doctor`][doctor] | Check health; repair nothing. |
|
|
166
|
+
| [`init`][init] | Scaffold and bind. |
|
|
167
|
+
| [`org init`][init] | Scaffold org memory. |
|
|
168
|
+
| [`enable`][sync] | Enable git sync. |
|
|
169
|
+
| [`clone`][sync] | Clone a memory remote. |
|
|
170
|
+
| [`pull`][sync] | Fast-forward from upstream. |
|
|
171
|
+
| [`push`][sync] | Commit selected files and push. |
|
|
172
|
+
| [`disable`][sync] | Disable sync. |
|
|
173
|
+
| [`archive`][archive-and-restore] | Archive a supplementary file. |
|
|
174
|
+
| [`restore`][archive-and-restore] | Restore to an empty slot. |
|
|
175
|
+
| [`setup`][setup] | Install runtime integration. |
|
|
176
|
+
| [`startup`][startup] | Print the metadata manifest. |
|
|
177
|
+
| [`capture`][capture-and-recall] | Record a decision. |
|
|
178
|
+
| [`recall`][capture-and-recall] | Read bounded context. |
|
|
179
|
+
| [`debrief write`][debrief-write] | Publish a session summary. |
|
|
180
|
+
|
|
181
|
+
[status]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#status
|
|
182
|
+
[doctor]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#doctor
|
|
183
|
+
[init]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#init
|
|
184
|
+
[sync]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#sync
|
|
185
|
+
[archive-and-restore]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#archive-and-restore
|
|
186
|
+
[setup]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#setup
|
|
187
|
+
[startup]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#startup
|
|
188
|
+
[capture-and-recall]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#capture-and-recall
|
|
189
|
+
[debrief-write]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#debrief-write
|
|
190
|
+
|
|
191
|
+
## Examples
|
|
192
|
+
|
|
193
|
+
Scratch run in `/private/tmp/am-readme-demo`: init/startup/recall excerpts;
|
|
194
|
+
other outputs complete.
|
|
195
|
+
|
|
196
|
+
```console
|
|
197
|
+
$ agent-memory init --home memory --project demo
|
|
198
|
+
Initialized memory home: /private/tmp/am-readme-demo/memory
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```console
|
|
202
|
+
$ agent-memory startup --home memory --project demo --runtime claude --max-chars 420
|
|
203
|
+
agent-memory startup (claude): home /private/tmp/am-readme-demo/memory (flag), project demo (flag)
|
|
204
|
+
Project memory, read in this order (states are readability only; no content is injected):
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```console
|
|
208
|
+
$ agent-memory capture 'Use SQLite for the cache.' --why 'no daemon' --agent codex --home memory --project demo
|
|
209
|
+
captured: /private/tmp/am-readme-demo/memory/projects/demo/memory/decision_log.md (## 2026-09-07)
|
|
210
|
+
- **Use SQLite for the cache.** Why: no daemon (codex, 2026-09-07T01:53:42Z)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```console
|
|
214
|
+
$ agent-memory recall --home memory --project demo --max-chars 850
|
|
215
|
+
## 2026-09-07
|
|
216
|
+
|
|
217
|
+
- **Use SQLite for the cache.** Why: no daemon (codex, 2026-09-07T01:53:42Z)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```console
|
|
221
|
+
$ agent-memory status --home memory
|
|
222
|
+
home: memory
|
|
223
|
+
source: flag
|
|
224
|
+
exists: yes
|
|
225
|
+
marker: absent
|
|
226
|
+
gitignore: canonical
|
|
227
|
+
org-memory: present
|
|
228
|
+
projects: 1 with a memory dir
|
|
229
|
+
sync: not configured
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```console
|
|
233
|
+
$ agent-memory doctor --home memory
|
|
234
|
+
[+] Org Memory
|
|
235
|
+
[+] org-memory/debriefs/ — present
|
|
236
|
+
[+] debriefs/ — empty store, nothing to validate
|
|
237
|
+
|
|
238
|
+
[-] Memory Sync
|
|
239
|
+
[-] .oacp-memory-repo — not configured; memory sync hooks are disabled
|
|
240
|
+
Run: agent-memory enable [--remote URL]
|
|
241
|
+
|
|
242
|
+
No issues found.
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Project
|
|
246
|
+
|
|
247
|
+
- [PyPI package: agent-memory-cli](https://pypi.org/project/agent-memory-cli/)
|
|
248
|
+
- [Source](https://github.com/kiloloop/agent-memory)
|
|
249
|
+
- [OACP](https://github.com/kiloloop/oacp)
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
Apache-2.0. See [LICENSE](https://github.com/kiloloop/agent-memory/blob/main/LICENSE).
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
Activate `.venv` before running the two `make` commands.
|
|
258
|
+
|
|
259
|
+
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
260
|
+
make preflight # lint, test, build
|
|
261
|
+
make wheel-check # install the built wheel in a throwaway venv and prove it operates a home
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
agent_memory/__init__.py,sha256=--LT_n5IN-qMqQNISjOU9x0avYFWh51_XwsL8pTPAYU,316
|
|
2
|
+
agent_memory/__main__.py,sha256=XNI_oqFGNB-_kBZ2_GgnTA43aWGFFftC-rjgEvEwrmM,215
|
|
3
|
+
agent_memory/archive.py,sha256=ABSpaii9ONDxyeaFAqjEDttrIjpFJBZAvrdKWR436jc,15737
|
|
4
|
+
agent_memory/cli.py,sha256=4NiqRFt0ZG2x3efww6fc9yQzq6P2YSoI-Y3fa_5E6oQ,19840
|
|
5
|
+
agent_memory/debrief.py,sha256=mWwPXTXRA7lbYzIk4e4qrUlXu34RZCAaEV1CeY_swSg,12447
|
|
6
|
+
agent_memory/doctor.py,sha256=_m9olIf90dNrJv8tFhSuquhMRdyfiaYtqOicNJV6EMQ,27913
|
|
7
|
+
agent_memory/git_runner.py,sha256=Uk4rGOx2NXoYmY__wUAuKR36dZWSQ-w_LpsI-jO26WY,2184
|
|
8
|
+
agent_memory/home.py,sha256=x3HpOh078AsERKS85ZhVzlCB1v_PCvWyUJlKsionBbQ,9724
|
|
9
|
+
agent_memory/layout.py,sha256=wq8kmXgn7cm0boou2Q9YJFsgEKcFUDtrwhg6iDI5TF4,7153
|
|
10
|
+
agent_memory/org.py,sha256=fjIB_J5uEHqZzLW3mYDL0m1BkSKHrTt11XQbNiN-CMc,8874
|
|
11
|
+
agent_memory/publication.py,sha256=AuZI-cCIKjmtqb_aUOAxEWlxOUrY8Xa1HGPApiGuYLE,19436
|
|
12
|
+
agent_memory/startup.py,sha256=Ks_8svX1XVaWnhMUlOHiKVWexX7-Eyq_EoqEnF5pppg,10697
|
|
13
|
+
agent_memory/status.py,sha256=r63nhpVlJXPeIxsvLABYhfAGjymx3WxOuHR78991-gM,5198
|
|
14
|
+
agent_memory/sync.py,sha256=EbGCPbFTtF159pxtyCnWOEj6z8ho1tnhLflnrGjYP4Y,26429
|
|
15
|
+
agent_memory/workflow.py,sha256=C7A9YFIN_tnr2M3WC4CIP1jfU_4_4czrb89dxIxJIVQ,11169
|
|
16
|
+
agent_memory/setup/__init__.py,sha256=ZRdh5ZKPqud2PRr2QGzLIiybeHSEFZKG79S0rhgYF4M,1048
|
|
17
|
+
agent_memory/setup/claude.py,sha256=w-3SgFiiE1hXc-rnVqIRGoAoYZ4gADsji2RkD-kQ1LY,1371
|
|
18
|
+
agent_memory/setup/codex.py,sha256=pZt4Ff7Qc5-WO12jlWjJb1-RIyT70HFIEzwb0K8YHOA,1702
|
|
19
|
+
agent_memory/setup/common.py,sha256=m1IgTRVDArh6WEGkcKPaNWsxNWxwaCOgt1WN4AUvOhM,40314
|
|
20
|
+
agent_memory/setup/legacy.py,sha256=50vqi_uhtdedl5P29VPQdm6yqnI10aFUmSFtgra1KJY,2198
|
|
21
|
+
agent_memory/templates/org-memory/decisions.md,sha256=Et5wl18CjH467IxFt4mZZLqd49iVJo_yW4KgoKUmNuo,287
|
|
22
|
+
agent_memory/templates/org-memory/recent.md,sha256=cf4gcoXaSpa914c69LGPrRZ61QvbIeSYt-hnFPQjpU0,389
|
|
23
|
+
agent_memory/templates/org-memory/rules.md,sha256=OpmFL4_ZSeb8mRjC8C607FHZXdcM2d8768mXGrO_VPY,283
|
|
24
|
+
agent_memory/templates/project-memory/decision_log.md,sha256=nNmLH635rBi7p26d8YsLcQfXE9ExyLHK4oNj8jvrWi8,96
|
|
25
|
+
agent_memory/templates/project-memory/known_debt.md,sha256=jM5CVGtAfheKjUK4eFA1eSaaE7yLBm9YlmGNHxDUGIY,240
|
|
26
|
+
agent_memory/templates/project-memory/open_threads.md,sha256=0FgTjhFLaZXy19tyJceCUQE_QY-8Lr48Vd0VumbkzRQ,28
|
|
27
|
+
agent_memory/templates/project-memory/project_facts.md,sha256=UIX7_vRJ-tQ5AIT_w1Lzj6n1jhBY4VxyPCLUBfA1-Jg,196
|
|
28
|
+
agent_memory/templates/workflow/SKILL.md,sha256=4GLaQP0xbjrDBy5vDJbnokv7wOgaHVT0SZsQlZkVqco,1980
|
|
29
|
+
agent_memory_cli-0.1.0.dist-info/METADATA,sha256=oXIHdljPWWKZyxlk5sMKeh6yEh_RCCkJjz-photRJwE,10379
|
|
30
|
+
agent_memory_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
31
|
+
agent_memory_cli-0.1.0.dist-info/entry_points.txt,sha256=mG_BjBbd8UY1fAkTlcl2sByYvElm35FtvqUziRuI96Q,55
|
|
32
|
+
agent_memory_cli-0.1.0.dist-info/licenses/LICENSE,sha256=MeRxiQ3KgmBtsOGLgCkYhT5P-aoktQxag0v5CwePUMs,11338
|
|
33
|
+
agent_memory_cli-0.1.0.dist-info/RECORD,,
|