forked from chauncygu/collection-claude-code-source-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscan.py
More file actions
144 lines (114 loc) · 5.21 KB
/
scan.py
File metadata and controls
144 lines (114 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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)