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>
This commit is contained in:
223
nano-claude-code/memory/store.py
Normal file
223
nano-claude-code/memory/store.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user