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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ agent-mem = agent_mem.cli:app
@@ -0,0 +1,4 @@
1
+ typer>=0.12
2
+
3
+ [mcp]
4
+ mcp>=0.3