Files
chauncygu 1d4ffa964d Update README.MD and add nano-claude-code v3.0 + original-source-code/src
- README.MD: add original-source-code and nano-claude-code sections, update
  overview table (4 subprojects), add v3.0 news entry, expand comparison table
  with memory/multi-agent/skills dimensions
- nano-claude-code v3.0: multi-agent package (multi_agent/), memory package
  (memory/), skill package (skill/) with built-in /commit and /review skills,
  context compression (compaction.py), tool registry plugin system, diff view,
  17 slash commands, 18 built-in tools, 101 tests (~5000 lines total)
- original-source-code/src: add raw TypeScript source tree (1884 files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 10:26:29 -07:00

224 lines
7.0 KiB
Python

"""File-based memory storage with user-level and project-level scopes.
Storage layout:
user scope : ~/.nano_claude/memory/<slug>.md
project scope : .nano_claude/memory/<slug>.md (relative to cwd)
MEMORY.md in each directory is the index file — rebuilt automatically after
every save/delete. It is loaded into the system prompt to give Claude an
overview of available memories.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
# ── Paths ──────────────────────────────────────────────────────────────────
USER_MEMORY_DIR = Path.home() / ".nano_claude" / "memory"
INDEX_FILENAME = "MEMORY.md"
# Maximum lines/bytes for the index file (mirrors Claude Code limits)
MAX_INDEX_LINES = 200
MAX_INDEX_BYTES = 25_000
def get_project_memory_dir() -> Path:
"""Return the project-local memory directory (relative to cwd)."""
return Path.cwd() / ".nano_claude" / "memory"
def get_memory_dir(scope: str = "user") -> Path:
"""Return the memory directory for the given scope.
Args:
scope: "user" (global ~/.nano_claude/memory) or
"project" (.nano_claude/memory relative to cwd)
"""
if scope == "project":
return get_project_memory_dir()
return USER_MEMORY_DIR
# ── Data model ─────────────────────────────────────────────────────────────
@dataclass
class MemoryEntry:
"""A single memory entry loaded from a .md file.
Attributes:
name: human-readable name (also the display title in the index)
description: short one-line description (used for relevance decisions)
type: "user" | "feedback" | "project" | "reference"
content: body text of the memory
file_path: absolute path to the .md file on disk
created: date string, e.g. "2026-04-02"
scope: "user" | "project" — which directory this was loaded from
"""
name: str
description: str
type: str
content: str
file_path: str = ""
created: str = ""
scope: str = "user"
# ── Helpers ────────────────────────────────────────────────────────────────
def _slugify(name: str) -> str:
"""Convert name to a filesystem-safe slug (max 60 chars)."""
s = name.lower().strip().replace(" ", "_")
s = re.sub(r"[^a-z0-9_]", "", s)
return s[:60]
def parse_frontmatter(text: str) -> tuple[dict, str]:
"""Parse ---\\nkey: value\\n---\\nbody format.
Returns:
(meta_dict, body_str)
"""
if not text.startswith("---"):
return {}, text
parts = text.split("---", 2)
if len(parts) < 3:
return {}, text
meta: dict = {}
for line in parts[1].strip().splitlines():
if ":" in line:
key, _, val = line.partition(":")
meta[key.strip()] = val.strip()
return meta, parts[2].strip()
def _format_entry_md(entry: MemoryEntry) -> str:
"""Render a MemoryEntry as a markdown file with YAML frontmatter."""
return (
f"---\n"
f"name: {entry.name}\n"
f"description: {entry.description}\n"
f"type: {entry.type}\n"
f"created: {entry.created}\n"
f"---\n"
f"{entry.content}\n"
)
# ── Core storage operations ────────────────────────────────────────────────
def save_memory(entry: MemoryEntry, scope: str = "user") -> None:
"""Write/update a memory file and rebuild the index for that scope.
If a memory with the same name (slug) already exists, it is overwritten.
Args:
entry: MemoryEntry to persist
scope: "user" or "project"
"""
mem_dir = get_memory_dir(scope)
mem_dir.mkdir(parents=True, exist_ok=True)
slug = _slugify(entry.name)
fp = mem_dir / f"{slug}.md"
fp.write_text(_format_entry_md(entry))
entry.file_path = str(fp)
entry.scope = scope
_rewrite_index(scope)
def delete_memory(name: str, scope: str = "user") -> None:
"""Remove the memory file matching name and rebuild the index.
No error if not found.
"""
mem_dir = get_memory_dir(scope)
slug = _slugify(name)
fp = mem_dir / f"{slug}.md"
if fp.exists():
fp.unlink()
_rewrite_index(scope)
def load_entries(scope: str = "user") -> list[MemoryEntry]:
"""Scan all .md files (except MEMORY.md) in a scope and return entries.
Returns:
List of MemoryEntry sorted alphabetically by name.
"""
mem_dir = get_memory_dir(scope)
if not mem_dir.exists():
return []
entries: list[MemoryEntry] = []
for fp in sorted(mem_dir.glob("*.md")):
if fp.name == INDEX_FILENAME:
continue
try:
text = fp.read_text()
except Exception:
continue
meta, body = parse_frontmatter(text)
entries.append(MemoryEntry(
name=meta.get("name", fp.stem),
description=meta.get("description", ""),
type=meta.get("type", "user"),
content=body,
file_path=str(fp),
created=meta.get("created", ""),
scope=scope,
))
return entries
def load_index(scope: str = "all") -> list[MemoryEntry]:
"""Load memory entries from one or both scopes.
Args:
scope: "user", "project", or "all" (both combined)
Returns:
List of MemoryEntry (user entries first, then project).
"""
if scope == "all":
return load_entries("user") + load_entries("project")
return load_entries(scope)
def search_memory(query: str, scope: str = "all") -> list[MemoryEntry]:
"""Case-insensitive keyword match on name + description + content.
Returns:
List of matching MemoryEntry objects.
"""
q = query.lower()
results = []
for entry in load_index(scope):
haystack = f"{entry.name} {entry.description} {entry.content}".lower()
if q in haystack:
results.append(entry)
return results
def _rewrite_index(scope: str) -> None:
"""Rebuild MEMORY.md for the given scope from all .md files in that dir."""
mem_dir = get_memory_dir(scope)
if not mem_dir.exists():
return
index_path = mem_dir / INDEX_FILENAME
entries = load_entries(scope)
lines = [
f"- [{e.name}]({Path(e.file_path).name}) — {e.description}"
for e in entries
]
index_path.write_text("\n".join(lines) + ("\n" if lines else ""))
def get_index_content(scope: str = "user") -> str:
"""Return raw MEMORY.md content for the given scope, or '' if absent."""
mem_dir = get_memory_dir(scope)
index_path = mem_dir / INDEX_FILENAME
if not index_path.exists():
return ""
return index_path.read_text().strip()