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:
144
nano-claude-code/memory/scan.py
Normal file
144
nano-claude-code/memory/scan.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Memory file scanning with mtime tracking and freshness/age helpers.
|
||||
|
||||
Mirrors the key ideas from Claude Code's memoryScan.ts and memoryAge.ts:
|
||||
- Scan memory directories, sort newest-first
|
||||
- Format a manifest for display or AI relevance selection
|
||||
- Report memory age in human-readable form ("today", "3 days ago")
|
||||
- Emit a staleness caveat for memories older than 1 day
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from .store import get_memory_dir, parse_frontmatter, INDEX_FILENAME
|
||||
|
||||
MAX_MEMORY_FILES = 200
|
||||
|
||||
|
||||
# ── Data model ─────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class MemoryHeader:
|
||||
"""Lightweight descriptor loaded from a memory file's frontmatter.
|
||||
|
||||
Attributes:
|
||||
filename: basename of the .md file
|
||||
file_path: absolute path
|
||||
mtime_s: modification time (seconds since epoch)
|
||||
description: value from frontmatter `description:` field
|
||||
type: value from frontmatter `type:` field
|
||||
scope: "user" or "project"
|
||||
"""
|
||||
filename: str
|
||||
file_path: str
|
||||
mtime_s: float
|
||||
description: str
|
||||
type: str
|
||||
scope: str
|
||||
|
||||
|
||||
# ── Scanning ───────────────────────────────────────────────────────────────
|
||||
|
||||
def scan_memory_dir(mem_dir: Path, scope: str) -> list[MemoryHeader]:
|
||||
"""Scan a single memory directory and return headers sorted newest-first.
|
||||
|
||||
Reads only the frontmatter (first ~30 lines) for efficiency.
|
||||
Silently skips unreadable files. Caps at MAX_MEMORY_FILES entries.
|
||||
"""
|
||||
if not mem_dir.is_dir():
|
||||
return []
|
||||
|
||||
headers: list[MemoryHeader] = []
|
||||
for fp in mem_dir.glob("*.md"):
|
||||
if fp.name == INDEX_FILENAME:
|
||||
continue
|
||||
try:
|
||||
stat = fp.stat()
|
||||
# Read only the first 30 lines for frontmatter
|
||||
lines = fp.read_text(errors="replace").splitlines()[:30]
|
||||
snippet = "\n".join(lines)
|
||||
meta, _ = parse_frontmatter(snippet)
|
||||
headers.append(MemoryHeader(
|
||||
filename=fp.name,
|
||||
file_path=str(fp),
|
||||
mtime_s=stat.st_mtime,
|
||||
description=meta.get("description", ""),
|
||||
type=meta.get("type", ""),
|
||||
scope=scope,
|
||||
))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
headers.sort(key=lambda h: h.mtime_s, reverse=True)
|
||||
return headers[:MAX_MEMORY_FILES]
|
||||
|
||||
|
||||
def scan_all_memories() -> list[MemoryHeader]:
|
||||
"""Scan both user and project memory directories, merged newest-first."""
|
||||
user_dir = get_memory_dir("user")
|
||||
proj_dir = get_memory_dir("project")
|
||||
|
||||
user_headers = scan_memory_dir(user_dir, "user")
|
||||
proj_headers = scan_memory_dir(proj_dir, "project")
|
||||
|
||||
combined = user_headers + proj_headers
|
||||
combined.sort(key=lambda h: h.mtime_s, reverse=True)
|
||||
return combined[:MAX_MEMORY_FILES]
|
||||
|
||||
|
||||
# ── Age / freshness ────────────────────────────────────────────────────────
|
||||
|
||||
def memory_age_days(mtime_s: float) -> int:
|
||||
"""Days since mtime_s (floor-rounded, clamped to 0 for future times)."""
|
||||
return max(0, math.floor((time.time() - mtime_s) / 86_400))
|
||||
|
||||
|
||||
def memory_age_str(mtime_s: float) -> str:
|
||||
"""Human-readable age: 'today', 'yesterday', or 'N days ago'."""
|
||||
d = memory_age_days(mtime_s)
|
||||
if d == 0:
|
||||
return "today"
|
||||
if d == 1:
|
||||
return "yesterday"
|
||||
return f"{d} days ago"
|
||||
|
||||
|
||||
def memory_freshness_text(mtime_s: float) -> str:
|
||||
"""Staleness caveat for memories older than 1 day (empty string if fresh).
|
||||
|
||||
Motivated by user reports of stale code-state memories (file:line
|
||||
citations to code that has since changed) being asserted as fact.
|
||||
"""
|
||||
d = memory_age_days(mtime_s)
|
||||
if d <= 1:
|
||||
return ""
|
||||
return (
|
||||
f"This memory is {d} days old. "
|
||||
"Memories are point-in-time observations, not live state — "
|
||||
"claims about code behavior or file:line citations may be outdated. "
|
||||
"Verify against current code before asserting as fact."
|
||||
)
|
||||
|
||||
|
||||
# ── Manifest formatting ────────────────────────────────────────────────────
|
||||
|
||||
def format_memory_manifest(headers: list[MemoryHeader]) -> str:
|
||||
"""Format a list of MemoryHeader as a text manifest.
|
||||
|
||||
Format per line: [type/scope] filename (age): description
|
||||
Example:
|
||||
[feedback/user] feedback_testing.md (3 days ago): Don't mock DB in tests
|
||||
[project/project] project_freeze.md (today): Merge freeze until 2026-04-10
|
||||
"""
|
||||
lines = []
|
||||
for h in headers:
|
||||
tag = f"[{h.type}/{h.scope}]" if h.type else f"[{h.scope}]"
|
||||
age = memory_age_str(h.mtime_s)
|
||||
if h.description:
|
||||
lines.append(f"- {tag} {h.filename} ({age}): {h.description}")
|
||||
else:
|
||||
lines.append(f"- {tag} {h.filename} ({age})")
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user