Add daily coding summary tool for Obsidian
Integrates with Zed editor history and Git commits to track daily coding activity. Writes idempotent `## Coding Work` sections into Obsidian daily notes with project breakdowns and commit stats.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""Read recent editor activity from Zed's SQLite database."""
|
||||
|
||||
import sqlite3
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class EditorSession:
|
||||
path: str
|
||||
mtime: int
|
||||
language: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectSummary:
|
||||
project: str
|
||||
files: List[Tuple[str, int]] # (relative_path, mtime)
|
||||
languages: set
|
||||
|
||||
|
||||
def _find_zed_db() -> Optional[Path]:
|
||||
"""Locate the most recent Zed stable database."""
|
||||
zed_db_dir = Path.home() / ".local" / "share" / "zed" / "db"
|
||||
if not zed_db_dir.exists():
|
||||
return None
|
||||
|
||||
candidates = []
|
||||
for sub in zed_db_dir.iterdir():
|
||||
db_file = sub / "db.sqlite"
|
||||
if db_file.exists():
|
||||
# Prefer stable, then preview, then anything else
|
||||
priority = 0
|
||||
if "stable" in sub.name:
|
||||
priority = 2
|
||||
elif "preview" in sub.name:
|
||||
priority = 1
|
||||
candidates.append((priority, db_file.stat().st_mtime, db_file))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
||||
return candidates[0][2]
|
||||
|
||||
|
||||
def _resolve_project(file_path: Path, code_dirs: List[Path]) -> Optional[str]:
|
||||
"""Return the project name for a file path based on code directories."""
|
||||
for code_dir in code_dirs:
|
||||
try:
|
||||
rel = file_path.relative_to(code_dir)
|
||||
parts = rel.parts
|
||||
if parts:
|
||||
return parts[0]
|
||||
except ValueError:
|
||||
continue
|
||||
# Fallback: use parent directory name
|
||||
return file_path.parent.name if file_path.parent != Path("/") else None
|
||||
|
||||
|
||||
def fetch_zed_activity(
|
||||
start_ts: int,
|
||||
end_ts: int,
|
||||
code_dirs: Optional[List[Path]] = None,
|
||||
) -> List[ProjectSummary]:
|
||||
"""Fetch editor activity from Zed for the given time range."""
|
||||
db_path = _find_zed_db()
|
||||
if not db_path:
|
||||
return []
|
||||
|
||||
if code_dirs is None:
|
||||
code_dirs = [Path.home() / "Code"]
|
||||
code_dirs = [d.expanduser().resolve() for d in code_dirs if d.exists()]
|
||||
|
||||
sessions: List[EditorSession] = []
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT path, mtime_seconds, language
|
||||
FROM editors
|
||||
WHERE mtime_seconds >= ? AND mtime_seconds < ?
|
||||
ORDER BY mtime_seconds DESC
|
||||
""",
|
||||
(start_ts, end_ts),
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
path_blob, mtime, language = row
|
||||
if not path_blob:
|
||||
continue
|
||||
try:
|
||||
path_str = (
|
||||
path_blob.decode("utf-8")
|
||||
if isinstance(path_blob, bytes)
|
||||
else str(path_blob)
|
||||
)
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
continue
|
||||
sessions.append(
|
||||
EditorSession(path=path_str, mtime=mtime, language=language)
|
||||
)
|
||||
except sqlite3.Error as e:
|
||||
print(f"Warning: could not read Zed database: {e}")
|
||||
return []
|
||||
|
||||
# Group by project
|
||||
by_project: Dict[str, List[EditorSession]] = defaultdict(list)
|
||||
for s in sessions:
|
||||
fp = Path(s.path)
|
||||
project = _resolve_project(fp, code_dirs)
|
||||
if not project:
|
||||
project = fp.parent.name
|
||||
by_project[project].append(s)
|
||||
|
||||
summaries = []
|
||||
for project, items in by_project.items():
|
||||
files = []
|
||||
seen = set()
|
||||
languages = set()
|
||||
for item in items:
|
||||
if item.language:
|
||||
languages.add(item.language)
|
||||
rel = item.path
|
||||
# Store unique files with their latest mtime
|
||||
key = rel
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
files.append((rel, item.mtime))
|
||||
# Sort by mtime descending
|
||||
files.sort(key=lambda x: x[1], reverse=True)
|
||||
summaries.append(
|
||||
ProjectSummary(project=project, files=files, languages=languages)
|
||||
)
|
||||
|
||||
# Sort projects by number of files, then by latest mtime
|
||||
summaries.sort(key=lambda s: (-len(s.files), -s.files[0][1] if s.files else 0))
|
||||
return summaries
|
||||
Reference in New Issue
Block a user