easy-agent-mem 0.3.0__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.
- easy_agent_mem-0.3.0/LICENSE +21 -0
- easy_agent_mem-0.3.0/PKG-INFO +94 -0
- easy_agent_mem-0.3.0/README.md +74 -0
- easy_agent_mem-0.3.0/pyproject.toml +32 -0
- easy_agent_mem-0.3.0/setup.cfg +4 -0
- easy_agent_mem-0.3.0/src/agent_mem/__init__.py +1 -0
- easy_agent_mem-0.3.0/src/agent_mem/cli.py +414 -0
- easy_agent_mem-0.3.0/src/agent_mem/config.py +62 -0
- easy_agent_mem-0.3.0/src/agent_mem/mcp_server.py +107 -0
- easy_agent_mem-0.3.0/src/agent_mem/memory.py +88 -0
- easy_agent_mem-0.3.0/src/easy_agent_mem.egg-info/PKG-INFO +94 -0
- easy_agent_mem-0.3.0/src/easy_agent_mem.egg-info/SOURCES.txt +14 -0
- easy_agent_mem-0.3.0/src/easy_agent_mem.egg-info/dependency_links.txt +1 -0
- easy_agent_mem-0.3.0/src/easy_agent_mem.egg-info/entry_points.txt +2 -0
- easy_agent_mem-0.3.0/src/easy_agent_mem.egg-info/requires.txt +4 -0
- easy_agent_mem-0.3.0/src/easy_agent_mem.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atharva
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy-agent-mem
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Persistent memory layer for AI coding agents (Cursor / VS Code). Auto-summarizes sessions to Markdown / Obsidian.
|
|
5
|
+
Author-email: Atharva <atharva.v.deo@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai,agent,memory,obsidian,claude,cursor,mcp
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: typer>=0.12
|
|
17
|
+
Provides-Extra: mcp
|
|
18
|
+
Requires-Dist: mcp>=0.3; extra == "mcp"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# agent-mem
|
|
22
|
+
|
|
23
|
+
**Persistent memory layer for AI coding agents** (Cursor, VS Code + Claude Code, etc.)
|
|
24
|
+
|
|
25
|
+
Tired of repeating context every time you start a new chat?
|
|
26
|
+
`agent-mem` automatically maintains a clean project memory file so your agent always knows what happened before - without bloating the context window.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Simple local fallback: `.agent-memory/memory.md`
|
|
31
|
+
- Optional Obsidian vault support (with full graph view, backlinks, Canvas)
|
|
32
|
+
- Auto-generates strong agent instruction rules
|
|
33
|
+
- Zero extra models or API keys needed
|
|
34
|
+
- Works with any MCP-compatible IDE (Cursor, VS Code, etc.)
|
|
35
|
+
- Extremely lightweight
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install easy-agent-mem
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 1. One-time setup
|
|
47
|
+
agent-mem init
|
|
48
|
+
|
|
49
|
+
# 2. (Recommended) Add the generated rules to your IDE
|
|
50
|
+
# - Cursor: Settings -> Custom Instructions
|
|
51
|
+
# - VS Code: Create CLAUDE.md or .claude/instructions.md in project root
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
During `init` you can:
|
|
55
|
+
|
|
56
|
+
- Provide an Obsidian vault path (optional)
|
|
57
|
+
- Or just press Enter to use the simple local `.agent-memory/memory.md` fallback
|
|
58
|
+
|
|
59
|
+
## How It Works
|
|
60
|
+
|
|
61
|
+
1. `agent-mem init` creates:
|
|
62
|
+
- `AGENT-MEM-RULES.md` (strong instructions for the agent)
|
|
63
|
+
- `.agent-memory/memory.md` (or Obsidian notes)
|
|
64
|
+
|
|
65
|
+
2. Add the rules from `AGENT-MEM-RULES.md` to your IDE's custom instructions.
|
|
66
|
+
|
|
67
|
+
3. From then on, your agent will:
|
|
68
|
+
- Read memory first in every new chat
|
|
69
|
+
- Summarize sessions when context gets long
|
|
70
|
+
- Keep a clean, persistent project history
|
|
71
|
+
|
|
72
|
+
## Example Usage in Chat
|
|
73
|
+
|
|
74
|
+
Tell your agent:
|
|
75
|
+
> "Summarize this session for memory"
|
|
76
|
+
|
|
77
|
+
It will create a clean summary and append it to memory. Then start a fresh chat - the agent will automatically load the latest memory.
|
|
78
|
+
|
|
79
|
+
## Commands
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
agent-mem init # Setup (Obsidian optional)
|
|
83
|
+
agent-mem status # Show current config
|
|
84
|
+
agent-mem --help # Full help
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Project Links
|
|
88
|
+
|
|
89
|
+
- PyPI: [https://pypi.org/project/easy-agent-mem/](https://pypi.org/project/easy-agent-mem/)
|
|
90
|
+
- Source: [https://github.com/atharvavdeo/agent-mem](https://github.com/atharvavdeo/agent-mem)
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# agent-mem
|
|
2
|
+
|
|
3
|
+
**Persistent memory layer for AI coding agents** (Cursor, VS Code + Claude Code, etc.)
|
|
4
|
+
|
|
5
|
+
Tired of repeating context every time you start a new chat?
|
|
6
|
+
`agent-mem` automatically maintains a clean project memory file so your agent always knows what happened before - without bloating the context window.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Simple local fallback: `.agent-memory/memory.md`
|
|
11
|
+
- Optional Obsidian vault support (with full graph view, backlinks, Canvas)
|
|
12
|
+
- Auto-generates strong agent instruction rules
|
|
13
|
+
- Zero extra models or API keys needed
|
|
14
|
+
- Works with any MCP-compatible IDE (Cursor, VS Code, etc.)
|
|
15
|
+
- Extremely lightweight
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install easy-agent-mem
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. One-time setup
|
|
27
|
+
agent-mem init
|
|
28
|
+
|
|
29
|
+
# 2. (Recommended) Add the generated rules to your IDE
|
|
30
|
+
# - Cursor: Settings -> Custom Instructions
|
|
31
|
+
# - VS Code: Create CLAUDE.md or .claude/instructions.md in project root
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
During `init` you can:
|
|
35
|
+
|
|
36
|
+
- Provide an Obsidian vault path (optional)
|
|
37
|
+
- Or just press Enter to use the simple local `.agent-memory/memory.md` fallback
|
|
38
|
+
|
|
39
|
+
## How It Works
|
|
40
|
+
|
|
41
|
+
1. `agent-mem init` creates:
|
|
42
|
+
- `AGENT-MEM-RULES.md` (strong instructions for the agent)
|
|
43
|
+
- `.agent-memory/memory.md` (or Obsidian notes)
|
|
44
|
+
|
|
45
|
+
2. Add the rules from `AGENT-MEM-RULES.md` to your IDE's custom instructions.
|
|
46
|
+
|
|
47
|
+
3. From then on, your agent will:
|
|
48
|
+
- Read memory first in every new chat
|
|
49
|
+
- Summarize sessions when context gets long
|
|
50
|
+
- Keep a clean, persistent project history
|
|
51
|
+
|
|
52
|
+
## Example Usage in Chat
|
|
53
|
+
|
|
54
|
+
Tell your agent:
|
|
55
|
+
> "Summarize this session for memory"
|
|
56
|
+
|
|
57
|
+
It will create a clean summary and append it to memory. Then start a fresh chat - the agent will automatically load the latest memory.
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
agent-mem init # Setup (Obsidian optional)
|
|
63
|
+
agent-mem status # Show current config
|
|
64
|
+
agent-mem --help # Full help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Project Links
|
|
68
|
+
|
|
69
|
+
- PyPI: [https://pypi.org/project/easy-agent-mem/](https://pypi.org/project/easy-agent-mem/)
|
|
70
|
+
- Source: [https://github.com/atharvavdeo/agent-mem](https://github.com/atharvavdeo/agent-mem)
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "easy-agent-mem"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Persistent memory layer for AI coding agents (Cursor / VS Code). Auto-summarizes sessions to Markdown / Obsidian."
|
|
5
|
+
authors = [{ name = "Atharva", email = "atharva.v.deo@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
license = { text = "MIT" }
|
|
9
|
+
keywords = ["ai", "agent", "memory", "obsidian", "claude", "cursor", "mcp"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Software Development :: Libraries",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"typer>=0.12",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
mcp = ["mcp>=0.3"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
agent-mem = "agent_mem.cli:app"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=45", "wheel"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from .config import CONFIG_FILE, get_config, save_config
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Dict, List
|
|
8
|
+
|
|
9
|
+
from .memory import get_fallback_memory_file
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _project_name_from_root(project_root: Path) -> str:
|
|
15
|
+
return project_root.resolve().name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _rules_body(project_name: str) -> str:
|
|
19
|
+
return f"""# Agent-Mem Rules
|
|
20
|
+
|
|
21
|
+
You have persistent project memory via agent-mem.
|
|
22
|
+
|
|
23
|
+
## Objective
|
|
24
|
+
|
|
25
|
+
- Keep durable project context across chats.
|
|
26
|
+
- Prefer stored memory over long chat history.
|
|
27
|
+
|
|
28
|
+
## Memory Source Of Truth
|
|
29
|
+
|
|
30
|
+
- Primary (default): .agent-memory/memory.md
|
|
31
|
+
- Optional: [vault_path]/Memory/Agent-Mem/ when Obsidian mode is configured
|
|
32
|
+
|
|
33
|
+
## Mandatory Start-Of-Session Workflow
|
|
34
|
+
|
|
35
|
+
1. Resolve project root and project name (root folder name).
|
|
36
|
+
2. Load latest memory before planning or coding.
|
|
37
|
+
3. If memory exists, treat it as authoritative project context.
|
|
38
|
+
4. If memory does not exist, continue normally and create or update it after meaningful progress.
|
|
39
|
+
|
|
40
|
+
## Mandatory Summarization Triggers
|
|
41
|
+
|
|
42
|
+
- Context pressure (roughly 15-20 turns)
|
|
43
|
+
- Major milestone completed
|
|
44
|
+
- Before ending session when important decisions were made
|
|
45
|
+
|
|
46
|
+
## Required Summary Structure
|
|
47
|
+
|
|
48
|
+
Use concise Markdown with these sections in order:
|
|
49
|
+
|
|
50
|
+
- Goal
|
|
51
|
+
- Outcome
|
|
52
|
+
- Key decisions (with rationale)
|
|
53
|
+
- Files changed (path + reason)
|
|
54
|
+
- Open tasks or blockers
|
|
55
|
+
- Next prioritized steps
|
|
56
|
+
|
|
57
|
+
## Memory Write Rules
|
|
58
|
+
|
|
59
|
+
- Append new information; do not delete prior history unless it is clearly obsolete and corrected.
|
|
60
|
+
- Be factual and specific; never invent decisions or changes.
|
|
61
|
+
- Never write secrets (tokens, API keys, passwords, private credentials).
|
|
62
|
+
|
|
63
|
+
## MCP Compatibility (Optional)
|
|
64
|
+
|
|
65
|
+
- If MCP tools are available: use query_memory, summarize_to_obsidian, list_recent_sessions.
|
|
66
|
+
- If MCP tools are unavailable: read and update .agent-memory/memory.md directly with the same structure.
|
|
67
|
+
|
|
68
|
+
## Project Name Rule
|
|
69
|
+
|
|
70
|
+
- Always use the repository root folder name as project_name.
|
|
71
|
+
|
|
72
|
+
Project name for this workspace: {project_name}
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cursor_rule_content(project_name: str) -> str:
|
|
77
|
+
return f"""---
|
|
78
|
+
description: Enforce agent-mem persistent memory workflow
|
|
79
|
+
alwaysApply: true
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
Follow AGENT-MEM-RULES.md in the repository root.
|
|
83
|
+
|
|
84
|
+
Critical reminders:
|
|
85
|
+
|
|
86
|
+
- Resolve project_name as: {project_name}
|
|
87
|
+
- Read .agent-memory/memory.md before planning or coding.
|
|
88
|
+
- On long sessions or milestones, write a structured summary with decisions and changed files.
|
|
89
|
+
- Use MCP tools when available; otherwise update memory.md directly.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _claude_instructions_content(project_name: str) -> str:
|
|
94
|
+
return f"""# Agent Memory Instructions
|
|
95
|
+
|
|
96
|
+
Use AGENT-MEM-RULES.md as the source of truth.
|
|
97
|
+
|
|
98
|
+
Project name: {project_name}
|
|
99
|
+
|
|
100
|
+
Mandatory behavior:
|
|
101
|
+
|
|
102
|
+
- Resolve project name from repository root and keep it consistent.
|
|
103
|
+
- Read memory before planning or coding.
|
|
104
|
+
- Summarize at milestones/context pressure and persist outcomes.
|
|
105
|
+
- Prefer memory content over old chat history.
|
|
106
|
+
- Never hallucinate historical decisions.
|
|
107
|
+
- Never store secrets in memory files.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _simple_wrapper_content(path_hint: str) -> str:
|
|
112
|
+
return (
|
|
113
|
+
"Use AGENT-MEM-RULES.md in the repository root as the authoritative instruction set for memory workflow.\n"
|
|
114
|
+
f"Wrapper target: {path_hint}\n"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _write_file(path: Path, content: str) -> bool:
|
|
119
|
+
if path.exists():
|
|
120
|
+
return False
|
|
121
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
path.write_text(content, encoding="utf-8")
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _create_instruction_files(project_root: Path) -> Dict[str, List[str]]:
|
|
127
|
+
project_name = _project_name_from_root(project_root)
|
|
128
|
+
|
|
129
|
+
files_to_write = {
|
|
130
|
+
project_root / "AGENT-MEM-RULES.md": _rules_body(project_name),
|
|
131
|
+
project_root / ".cursor" / "rules" / "agent-mem.mdc": _cursor_rule_content(project_name),
|
|
132
|
+
project_root / ".claude" / "instructions.md": _claude_instructions_content(project_name),
|
|
133
|
+
project_root / "CLAUDE.md": _claude_instructions_content(project_name),
|
|
134
|
+
project_root / ".antigravity" / "rules.md": _simple_wrapper_content("antigravity"),
|
|
135
|
+
project_root / ".opencode" / "instructions.md": _simple_wrapper_content("opencode"),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
created: List[str] = []
|
|
139
|
+
skipped: List[str] = []
|
|
140
|
+
for path, content in files_to_write.items():
|
|
141
|
+
if _write_file(path, content):
|
|
142
|
+
created.append(str(path.relative_to(project_root)))
|
|
143
|
+
else:
|
|
144
|
+
skipped.append(str(path.relative_to(project_root)))
|
|
145
|
+
|
|
146
|
+
return {"created": created, "skipped": skipped}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _upsert_mcp_config(config_path: Path) -> str:
|
|
150
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
|
|
152
|
+
if config_path.exists():
|
|
153
|
+
try:
|
|
154
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
155
|
+
if not isinstance(config, dict):
|
|
156
|
+
config = {}
|
|
157
|
+
except json.JSONDecodeError:
|
|
158
|
+
config = {}
|
|
159
|
+
else:
|
|
160
|
+
config = {}
|
|
161
|
+
|
|
162
|
+
servers = config.get("mcpServers")
|
|
163
|
+
if not isinstance(servers, dict):
|
|
164
|
+
servers = {}
|
|
165
|
+
|
|
166
|
+
resolved_command = shutil.which("agent-mem") or "agent-mem"
|
|
167
|
+
servers["agent-mem"] = {
|
|
168
|
+
"command": resolved_command,
|
|
169
|
+
"args": ["serve"],
|
|
170
|
+
}
|
|
171
|
+
config["mcpServers"] = servers
|
|
172
|
+
|
|
173
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
174
|
+
return str(config_path)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _create_local_mcp_configs(project_root: Path) -> List[str]:
|
|
178
|
+
return [
|
|
179
|
+
_upsert_mcp_config(project_root / ".vscode" / "mcp.json"),
|
|
180
|
+
_upsert_mcp_config(project_root / ".cursor" / "mcp.json"),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _upsert_vscode_mcp_with_python(config_path: Path, python_executable: str) -> str:
|
|
185
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
if config_path.exists():
|
|
188
|
+
try:
|
|
189
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
190
|
+
if not isinstance(config, dict):
|
|
191
|
+
config = {}
|
|
192
|
+
except json.JSONDecodeError:
|
|
193
|
+
config = {}
|
|
194
|
+
else:
|
|
195
|
+
config = {}
|
|
196
|
+
|
|
197
|
+
servers = config.get("mcpServers")
|
|
198
|
+
if not isinstance(servers, dict):
|
|
199
|
+
servers = {}
|
|
200
|
+
|
|
201
|
+
servers["agent-mem"] = {
|
|
202
|
+
"command": python_executable,
|
|
203
|
+
"args": ["-m", "agent_mem.cli", "serve"],
|
|
204
|
+
}
|
|
205
|
+
config["mcpServers"] = servers
|
|
206
|
+
|
|
207
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
208
|
+
return str(config_path)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _detect_preferred_python(project_root: Path) -> str:
|
|
212
|
+
# Prefer repo-local virtual env for predictable MCP startup.
|
|
213
|
+
local_venv_python = project_root / ".venv" / "bin" / "python"
|
|
214
|
+
if local_venv_python.exists():
|
|
215
|
+
return str(local_venv_python)
|
|
216
|
+
|
|
217
|
+
virtual_env = Path(sys.prefix)
|
|
218
|
+
if (virtual_env / "bin" / "python").exists():
|
|
219
|
+
return str(virtual_env / "bin" / "python")
|
|
220
|
+
|
|
221
|
+
return sys.executable
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _resolve_python_for_mcp(project_root: Path, python_option: str) -> str:
|
|
225
|
+
if python_option.strip():
|
|
226
|
+
explicit_python = Path(python_option.strip()).expanduser().resolve()
|
|
227
|
+
if not explicit_python.exists():
|
|
228
|
+
typer.echo(f"❌ Python path not found: {explicit_python}", err=True)
|
|
229
|
+
raise typer.Exit(1)
|
|
230
|
+
return str(explicit_python)
|
|
231
|
+
|
|
232
|
+
return _detect_preferred_python(project_root)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _build_mcp_json_with_python(python_executable: str) -> dict:
|
|
236
|
+
return {
|
|
237
|
+
"mcpServers": {
|
|
238
|
+
"agent-mem": {
|
|
239
|
+
"command": python_executable,
|
|
240
|
+
"args": ["-m", "agent_mem.cli", "serve"],
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _ensure_local_memory_initialized(project_root: Path):
|
|
247
|
+
fallback_file = get_fallback_memory_file(project_root)
|
|
248
|
+
if fallback_file.exists():
|
|
249
|
+
return
|
|
250
|
+
fallback_file.parent.mkdir(parents=True, exist_ok=True)
|
|
251
|
+
fallback_file.write_text("# Agent Memory\n\n", encoding="utf-8")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@app.command()
|
|
255
|
+
def init():
|
|
256
|
+
"""One-time setup - Obsidian is optional, local memory.md fallback is supported."""
|
|
257
|
+
typer.echo("agent-mem setup (Obsidian path is optional)")
|
|
258
|
+
vault = typer.prompt(
|
|
259
|
+
"Full path to your Obsidian vault (press Enter to skip and use local memory.md)",
|
|
260
|
+
default="",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
config = {"use_obsidian": False, "obsidian_vault": None}
|
|
264
|
+
if vault.strip():
|
|
265
|
+
vault_path = Path(vault).expanduser().resolve()
|
|
266
|
+
if vault_path.exists():
|
|
267
|
+
config = {"use_obsidian": True, "obsidian_vault": str(vault_path)}
|
|
268
|
+
typer.echo(f"✅ Using Obsidian vault: {vault_path}")
|
|
269
|
+
else:
|
|
270
|
+
typer.echo("⚠️ Path does not exist. Falling back to local memory.md", err=True)
|
|
271
|
+
else:
|
|
272
|
+
typer.echo("✅ Using simple local memory.md fallback in project folder")
|
|
273
|
+
_ensure_local_memory_initialized(Path.cwd().resolve())
|
|
274
|
+
|
|
275
|
+
save_config(config)
|
|
276
|
+
typer.echo(f"✅ Config saved to {CONFIG_FILE}")
|
|
277
|
+
|
|
278
|
+
project_root = Path.cwd().resolve()
|
|
279
|
+
typer.echo(f"Project root for setup: {project_root}")
|
|
280
|
+
|
|
281
|
+
if typer.confirm(
|
|
282
|
+
"Create automatic instruction files for Cursor/Claude/Antigravity/OpenCode? (recommended)",
|
|
283
|
+
default=True,
|
|
284
|
+
):
|
|
285
|
+
result = _create_instruction_files(project_root)
|
|
286
|
+
if result["created"]:
|
|
287
|
+
typer.echo("✅ Created instruction files:")
|
|
288
|
+
for relative_path in result["created"]:
|
|
289
|
+
typer.echo(f" - {relative_path}")
|
|
290
|
+
if result["skipped"]:
|
|
291
|
+
typer.echo("ℹ️ Existing files kept unchanged:")
|
|
292
|
+
for relative_path in result["skipped"]:
|
|
293
|
+
typer.echo(f" - {relative_path}")
|
|
294
|
+
typer.echo("Restart your IDE chat (or reload rules) to apply instruction changes.")
|
|
295
|
+
|
|
296
|
+
if typer.confirm(
|
|
297
|
+
"Create/update local MCP config files (.vscode/mcp.json and .cursor/mcp.json)? (optional)",
|
|
298
|
+
default=False,
|
|
299
|
+
):
|
|
300
|
+
written_paths = _create_local_mcp_configs(project_root)
|
|
301
|
+
typer.echo("✅ MCP config updated:")
|
|
302
|
+
for config_path in written_paths:
|
|
303
|
+
typer.echo(f" - {config_path}")
|
|
304
|
+
|
|
305
|
+
typer.echo("\nSetup complete!")
|
|
306
|
+
typer.echo("Next: add AGENT-MEM-RULES.md to your IDE custom instructions.")
|
|
307
|
+
typer.echo('Optional MCP mode: pip install "easy-agent-mem[mcp]" && agent-mem serve')
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@app.command()
|
|
311
|
+
def setup():
|
|
312
|
+
"""Set up instruction files + MCP configs for the current project."""
|
|
313
|
+
project_root = Path.cwd().resolve()
|
|
314
|
+
|
|
315
|
+
result = _create_instruction_files(project_root)
|
|
316
|
+
written_paths = _create_local_mcp_configs(project_root)
|
|
317
|
+
|
|
318
|
+
typer.echo(f"Project root: {project_root}")
|
|
319
|
+
if result["created"]:
|
|
320
|
+
typer.echo("✅ Created instruction files:")
|
|
321
|
+
for relative_path in result["created"]:
|
|
322
|
+
typer.echo(f" - {relative_path}")
|
|
323
|
+
if result["skipped"]:
|
|
324
|
+
typer.echo("ℹ️ Existing files kept unchanged:")
|
|
325
|
+
for relative_path in result["skipped"]:
|
|
326
|
+
typer.echo(f" - {relative_path}")
|
|
327
|
+
|
|
328
|
+
typer.echo("✅ MCP config updated:")
|
|
329
|
+
for config_path in written_paths:
|
|
330
|
+
typer.echo(f" - {config_path}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@app.command("setup-vscode")
|
|
334
|
+
def setup_vscode(
|
|
335
|
+
python: str = typer.Option(
|
|
336
|
+
"",
|
|
337
|
+
"--python",
|
|
338
|
+
help="Explicit Python executable path for MCP command",
|
|
339
|
+
),
|
|
340
|
+
):
|
|
341
|
+
"""Write .vscode/mcp.json using the currently active Python interpreter."""
|
|
342
|
+
project_root = Path.cwd().resolve()
|
|
343
|
+
config_path = project_root / ".vscode" / "mcp.json"
|
|
344
|
+
selected_python = _resolve_python_for_mcp(project_root, python)
|
|
345
|
+
|
|
346
|
+
written = _upsert_vscode_mcp_with_python(config_path, selected_python)
|
|
347
|
+
|
|
348
|
+
typer.echo("✅ VS Code MCP config written")
|
|
349
|
+
typer.echo(f"Path: {written}")
|
|
350
|
+
typer.echo("Server command uses:")
|
|
351
|
+
typer.echo(f" {selected_python} -m agent_mem.cli serve")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@app.command("print-mcp-json")
|
|
355
|
+
def print_mcp_json(
|
|
356
|
+
python: str = typer.Option(
|
|
357
|
+
"",
|
|
358
|
+
"--python",
|
|
359
|
+
help="Explicit Python executable path for MCP command",
|
|
360
|
+
),
|
|
361
|
+
):
|
|
362
|
+
"""Print a ready-to-paste MCP JSON block without writing any files."""
|
|
363
|
+
project_root = Path.cwd().resolve()
|
|
364
|
+
selected_python = _resolve_python_for_mcp(project_root, python)
|
|
365
|
+
block = _build_mcp_json_with_python(selected_python)
|
|
366
|
+
|
|
367
|
+
typer.echo(json.dumps(block, indent=2))
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@app.command()
|
|
371
|
+
def serve():
|
|
372
|
+
"""Start the optional MCP server (stdio transport)."""
|
|
373
|
+
try:
|
|
374
|
+
from .mcp_server import mcp
|
|
375
|
+
except ImportError:
|
|
376
|
+
typer.echo(
|
|
377
|
+
'❌ MCP dependencies are not installed. Install with: pip install "easy-agent-mem[mcp]"',
|
|
378
|
+
err=True,
|
|
379
|
+
)
|
|
380
|
+
raise typer.Exit(1)
|
|
381
|
+
|
|
382
|
+
config = get_config()
|
|
383
|
+
if config.get("use_obsidian") and not config.get("obsidian_vault"):
|
|
384
|
+
typer.echo("❌ Obsidian mode selected but no vault path found. Re-run agent-mem init.", err=True)
|
|
385
|
+
raise typer.Exit(1)
|
|
386
|
+
|
|
387
|
+
typer.echo("agent-mem MCP server started (stdio transport)")
|
|
388
|
+
typer.echo("Tip: run 'agent-mem print-mcp-json' if you need a config block.")
|
|
389
|
+
mcp.run()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@app.command()
|
|
393
|
+
def status():
|
|
394
|
+
"""Show the configured vault and stored session count."""
|
|
395
|
+
config = get_config()
|
|
396
|
+
project_root = Path.cwd().resolve()
|
|
397
|
+
vault = config.get("obsidian_vault")
|
|
398
|
+
|
|
399
|
+
if config.get("use_obsidian") and vault:
|
|
400
|
+
memory_dir = Path(vault) / "Memory" / "Agent-Mem"
|
|
401
|
+
count = len(list(memory_dir.glob("*.md"))) if memory_dir.exists() else 0
|
|
402
|
+
typer.echo("Storage mode : Obsidian")
|
|
403
|
+
typer.echo(f"Obsidian vault : {vault}")
|
|
404
|
+
typer.echo(f"Memory notes : {count} sessions stored")
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
fallback_file = project_root / ".agent-memory" / "memory.md"
|
|
408
|
+
typer.echo("Storage mode : Local fallback")
|
|
409
|
+
typer.echo(f"Memory file : {fallback_file}")
|
|
410
|
+
typer.echo(f"Memory exists : {'yes' if fallback_file.exists() else 'no'}")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
app()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 11):
|
|
8
|
+
tomllib = importlib.import_module("tomllib")
|
|
9
|
+
else: # pragma: no cover
|
|
10
|
+
tomllib = importlib.import_module("tomli")
|
|
11
|
+
|
|
12
|
+
CONFIG_DIR = Path.home() / ".config" / "agent-mem"
|
|
13
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
14
|
+
DEFAULT_CONFIG = {"use_obsidian": False, "obsidian_vault": None}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_config(config: dict) -> dict:
|
|
18
|
+
normalized = dict(DEFAULT_CONFIG)
|
|
19
|
+
normalized.update(config)
|
|
20
|
+
|
|
21
|
+
use_obsidian = normalized.get("use_obsidian")
|
|
22
|
+
if isinstance(use_obsidian, str):
|
|
23
|
+
normalized["use_obsidian"] = use_obsidian.lower() in {"1", "true", "yes", "y"}
|
|
24
|
+
|
|
25
|
+
vault = normalized.get("obsidian_vault")
|
|
26
|
+
if isinstance(vault, str) and not vault.strip():
|
|
27
|
+
normalized["obsidian_vault"] = None
|
|
28
|
+
|
|
29
|
+
if not normalized.get("obsidian_vault"):
|
|
30
|
+
normalized["use_obsidian"] = False
|
|
31
|
+
|
|
32
|
+
return normalized
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _serialize_toml(config: dict) -> str:
|
|
36
|
+
lines = []
|
|
37
|
+
for key, value in config.items():
|
|
38
|
+
if value is None:
|
|
39
|
+
value_repr = '""'
|
|
40
|
+
elif isinstance(value, bool):
|
|
41
|
+
value_repr = "true" if value else "false"
|
|
42
|
+
elif isinstance(value, (int, float)):
|
|
43
|
+
value_repr = str(value)
|
|
44
|
+
else:
|
|
45
|
+
escaped = str(value).replace("\\", "\\\\").replace('"', '\\"')
|
|
46
|
+
value_repr = f'"{escaped}"'
|
|
47
|
+
lines.append(f"{key} = {value_repr}")
|
|
48
|
+
return "\n".join(lines) + "\n"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_config() -> dict:
|
|
52
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
if not CONFIG_FILE.exists():
|
|
54
|
+
return dict(DEFAULT_CONFIG)
|
|
55
|
+
|
|
56
|
+
with open(CONFIG_FILE, "rb") as file:
|
|
57
|
+
loaded = tomllib.load(file)
|
|
58
|
+
return _normalize_config(loaded)
|
|
59
|
+
|
|
60
|
+
def save_config(config: dict):
|
|
61
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
CONFIG_FILE.write_text(_serialize_toml(_normalize_config(config)), encoding="utf-8")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from mcp.server.fastmcp import FastMCP
|
|
3
|
+
except ImportError: # pragma: no cover - fallback for older installs
|
|
4
|
+
from fastmcp import FastMCP
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .memory import (
|
|
9
|
+
get_fallback_memory_file,
|
|
10
|
+
is_obsidian_enabled,
|
|
11
|
+
list_recent_session_files,
|
|
12
|
+
write_session_summary,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
mcp = FastMCP("agent-mem")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _score_line(line: str, query: str) -> int:
|
|
19
|
+
line_lower = line.lower()
|
|
20
|
+
query_lower = query.lower().strip()
|
|
21
|
+
if not query_lower:
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
score = 0
|
|
25
|
+
for word in query_lower.split():
|
|
26
|
+
if word and word in line_lower:
|
|
27
|
+
score += 2
|
|
28
|
+
if query_lower in line_lower:
|
|
29
|
+
score += 5
|
|
30
|
+
return score
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _scored_excerpt(content: str, query: str, limit: int = 12) -> list[str]:
|
|
34
|
+
lines = [line.strip() for line in content.splitlines() if line.strip()]
|
|
35
|
+
scored = sorted(
|
|
36
|
+
((line, _score_line(line, query)) for line in lines),
|
|
37
|
+
key=lambda item: item[1],
|
|
38
|
+
reverse=True,
|
|
39
|
+
)
|
|
40
|
+
matches = [line for line, score in scored if score > 0][:limit]
|
|
41
|
+
if matches:
|
|
42
|
+
return matches
|
|
43
|
+
return [content[:800].strip()] if content.strip() else []
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
def query_memory(project_name: str, query: str) -> str:
|
|
47
|
+
"""MUST be called at the start of every new session.
|
|
48
|
+
|
|
49
|
+
Use the project root folder name for project_name and a short goal-focused query.
|
|
50
|
+
Works in both modes:
|
|
51
|
+
- Obsidian mode: reads recent notes from <vault>/Memory/Agent-Mem
|
|
52
|
+
- Fallback mode: reads local .agent-memory/memory.md
|
|
53
|
+
"""
|
|
54
|
+
project_root = Path.cwd().resolve()
|
|
55
|
+
files = list_recent_session_files(project_name, count=5, project_root=project_root)
|
|
56
|
+
|
|
57
|
+
if not files:
|
|
58
|
+
return "No memory yet for this project. Start by chatting, then save a session summary."
|
|
59
|
+
|
|
60
|
+
mode = "Obsidian" if is_obsidian_enabled() else "Local memory.md"
|
|
61
|
+
result = [f"## Latest Memory ({mode})"]
|
|
62
|
+
for file_path in files:
|
|
63
|
+
content = file_path.read_text(encoding="utf-8")
|
|
64
|
+
matches = _scored_excerpt(content, query, limit=12)
|
|
65
|
+
|
|
66
|
+
result.append(f"### {file_path.name}")
|
|
67
|
+
if matches:
|
|
68
|
+
result.extend(matches)
|
|
69
|
+
result.append("")
|
|
70
|
+
|
|
71
|
+
return "\n".join(result)[:6000]
|
|
72
|
+
|
|
73
|
+
@mcp.tool()
|
|
74
|
+
def summarize_to_obsidian(project_name: str, summary: str) -> str:
|
|
75
|
+
"""Call when context is getting long (about 15-20 turns).
|
|
76
|
+
|
|
77
|
+
Provide a structured markdown summary with decisions, file changes, open tasks,
|
|
78
|
+
blockers, and next steps. After saving, suggest starting a fresh chat.
|
|
79
|
+
|
|
80
|
+
This tool writes to Obsidian when configured, otherwise to local .agent-memory/memory.md.
|
|
81
|
+
"""
|
|
82
|
+
project_root = Path.cwd().resolve()
|
|
83
|
+
filepath = write_session_summary(project_name, summary, project_root=project_root)
|
|
84
|
+
target = "Obsidian" if is_obsidian_enabled() else "local memory file"
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
f"✅ Session summarized and saved to {target}: {filepath}\n"
|
|
88
|
+
"You can now start a fresh chat. Future chats will use query_memory first."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@mcp.tool()
|
|
93
|
+
def list_recent_sessions(project_name: str, count: int = 3) -> str:
|
|
94
|
+
"""List recent memory notes for quick historical overviews.
|
|
95
|
+
|
|
96
|
+
Useful when users ask about prior work before requesting deep details.
|
|
97
|
+
In fallback mode, returns the local memory.md file when available.
|
|
98
|
+
"""
|
|
99
|
+
project_root = Path.cwd().resolve()
|
|
100
|
+
files = list_recent_session_files(project_name, count=count, project_root=project_root)
|
|
101
|
+
if not files:
|
|
102
|
+
return "No sessions yet"
|
|
103
|
+
if is_obsidian_enabled():
|
|
104
|
+
return "\n".join(file_path.name for file_path in files)
|
|
105
|
+
|
|
106
|
+
fallback = get_fallback_memory_file(project_root)
|
|
107
|
+
return str(fallback.relative_to(project_root)) if fallback.exists() else "No sessions yet"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .config import get_config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
FALLBACK_DIR_NAME = ".agent-memory"
|
|
8
|
+
FALLBACK_FILE_NAME = "memory.md"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_obsidian_enabled() -> bool:
|
|
12
|
+
config = get_config()
|
|
13
|
+
return bool(config.get("use_obsidian") and config.get("obsidian_vault"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_memory_dir(project_root: Path | None = None) -> Path:
|
|
17
|
+
resolved_project_root = (project_root or Path.cwd()).resolve()
|
|
18
|
+
if is_obsidian_enabled():
|
|
19
|
+
vault = Path(get_config()["obsidian_vault"])
|
|
20
|
+
memory_dir = vault / "Memory" / "Agent-Mem"
|
|
21
|
+
else:
|
|
22
|
+
memory_dir = resolved_project_root / FALLBACK_DIR_NAME
|
|
23
|
+
|
|
24
|
+
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
return memory_dir
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_fallback_memory_file(project_root: Path | None = None) -> Path:
|
|
29
|
+
return get_memory_dir(project_root) / FALLBACK_FILE_NAME
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _session_block(project_name: str, summary: str) -> str:
|
|
33
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
34
|
+
return f"""## Session Summary - {timestamp}
|
|
35
|
+
|
|
36
|
+
Project: {project_name}
|
|
37
|
+
|
|
38
|
+
{summary}
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
**Auto-generated by agent-mem**
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def write_session_summary(project_name: str, summary: str, project_root: Path | None = None) -> str:
|
|
46
|
+
resolved_project_root = (project_root or Path.cwd()).resolve()
|
|
47
|
+
memory_dir = get_memory_dir(resolved_project_root)
|
|
48
|
+
date_str = datetime.now().strftime("%Y-%m-%d_%H-%M")
|
|
49
|
+
|
|
50
|
+
if is_obsidian_enabled():
|
|
51
|
+
filepath = memory_dir / f"{project_name}-{date_str}-session.md"
|
|
52
|
+
content = f"""# Session Summary - {datetime.now().strftime("%Y-%m-%d %H:%M")}
|
|
53
|
+
|
|
54
|
+
{summary}
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
**Auto-generated by agent-mem • Obsidian mode**
|
|
58
|
+
"""
|
|
59
|
+
filepath.write_text(content, encoding="utf-8")
|
|
60
|
+
vault = Path(get_config()["obsidian_vault"])
|
|
61
|
+
return str(filepath.relative_to(vault))
|
|
62
|
+
|
|
63
|
+
filepath = get_fallback_memory_file(resolved_project_root)
|
|
64
|
+
entry = _session_block(project_name, summary).strip() + "\n"
|
|
65
|
+
if filepath.exists():
|
|
66
|
+
filepath.write_text(filepath.read_text(encoding="utf-8") + "\n" + entry, encoding="utf-8")
|
|
67
|
+
else:
|
|
68
|
+
filepath.write_text("# Agent Memory\n\n" + entry, encoding="utf-8")
|
|
69
|
+
return str(filepath.relative_to(resolved_project_root))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def list_recent_session_files(
|
|
73
|
+
project_name: str,
|
|
74
|
+
count: int = 3,
|
|
75
|
+
project_root: Path | None = None,
|
|
76
|
+
) -> list[Path]:
|
|
77
|
+
resolved_project_root = (project_root or Path.cwd()).resolve()
|
|
78
|
+
if is_obsidian_enabled():
|
|
79
|
+
memory_dir = get_memory_dir(resolved_project_root)
|
|
80
|
+
files = sorted(
|
|
81
|
+
memory_dir.glob(f"{project_name}*.md"),
|
|
82
|
+
key=lambda path: path.stat().st_mtime,
|
|
83
|
+
reverse=True,
|
|
84
|
+
)
|
|
85
|
+
return files[:count]
|
|
86
|
+
|
|
87
|
+
fallback_file = get_fallback_memory_file(resolved_project_root)
|
|
88
|
+
return [fallback_file] if fallback_file.exists() else []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy-agent-mem
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Persistent memory layer for AI coding agents (Cursor / VS Code). Auto-summarizes sessions to Markdown / Obsidian.
|
|
5
|
+
Author-email: Atharva <atharva.v.deo@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai,agent,memory,obsidian,claude,cursor,mcp
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: typer>=0.12
|
|
17
|
+
Provides-Extra: mcp
|
|
18
|
+
Requires-Dist: mcp>=0.3; extra == "mcp"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# agent-mem
|
|
22
|
+
|
|
23
|
+
**Persistent memory layer for AI coding agents** (Cursor, VS Code + Claude Code, etc.)
|
|
24
|
+
|
|
25
|
+
Tired of repeating context every time you start a new chat?
|
|
26
|
+
`agent-mem` automatically maintains a clean project memory file so your agent always knows what happened before - without bloating the context window.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Simple local fallback: `.agent-memory/memory.md`
|
|
31
|
+
- Optional Obsidian vault support (with full graph view, backlinks, Canvas)
|
|
32
|
+
- Auto-generates strong agent instruction rules
|
|
33
|
+
- Zero extra models or API keys needed
|
|
34
|
+
- Works with any MCP-compatible IDE (Cursor, VS Code, etc.)
|
|
35
|
+
- Extremely lightweight
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install easy-agent-mem
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 1. One-time setup
|
|
47
|
+
agent-mem init
|
|
48
|
+
|
|
49
|
+
# 2. (Recommended) Add the generated rules to your IDE
|
|
50
|
+
# - Cursor: Settings -> Custom Instructions
|
|
51
|
+
# - VS Code: Create CLAUDE.md or .claude/instructions.md in project root
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
During `init` you can:
|
|
55
|
+
|
|
56
|
+
- Provide an Obsidian vault path (optional)
|
|
57
|
+
- Or just press Enter to use the simple local `.agent-memory/memory.md` fallback
|
|
58
|
+
|
|
59
|
+
## How It Works
|
|
60
|
+
|
|
61
|
+
1. `agent-mem init` creates:
|
|
62
|
+
- `AGENT-MEM-RULES.md` (strong instructions for the agent)
|
|
63
|
+
- `.agent-memory/memory.md` (or Obsidian notes)
|
|
64
|
+
|
|
65
|
+
2. Add the rules from `AGENT-MEM-RULES.md` to your IDE's custom instructions.
|
|
66
|
+
|
|
67
|
+
3. From then on, your agent will:
|
|
68
|
+
- Read memory first in every new chat
|
|
69
|
+
- Summarize sessions when context gets long
|
|
70
|
+
- Keep a clean, persistent project history
|
|
71
|
+
|
|
72
|
+
## Example Usage in Chat
|
|
73
|
+
|
|
74
|
+
Tell your agent:
|
|
75
|
+
> "Summarize this session for memory"
|
|
76
|
+
|
|
77
|
+
It will create a clean summary and append it to memory. Then start a fresh chat - the agent will automatically load the latest memory.
|
|
78
|
+
|
|
79
|
+
## Commands
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
agent-mem init # Setup (Obsidian optional)
|
|
83
|
+
agent-mem status # Show current config
|
|
84
|
+
agent-mem --help # Full help
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Project Links
|
|
88
|
+
|
|
89
|
+
- PyPI: [https://pypi.org/project/easy-agent-mem/](https://pypi.org/project/easy-agent-mem/)
|
|
90
|
+
- Source: [https://github.com/atharvavdeo/agent-mem](https://github.com/atharvavdeo/agent-mem)
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/agent_mem/__init__.py
|
|
5
|
+
src/agent_mem/cli.py
|
|
6
|
+
src/agent_mem/config.py
|
|
7
|
+
src/agent_mem/mcp_server.py
|
|
8
|
+
src/agent_mem/memory.py
|
|
9
|
+
src/easy_agent_mem.egg-info/PKG-INFO
|
|
10
|
+
src/easy_agent_mem.egg-info/SOURCES.txt
|
|
11
|
+
src/easy_agent_mem.egg-info/dependency_links.txt
|
|
12
|
+
src/easy_agent_mem.egg-info/entry_points.txt
|
|
13
|
+
src/easy_agent_mem.egg-info/requires.txt
|
|
14
|
+
src/easy_agent_mem.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent_mem
|