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:
2026-05-26 14:52:09 +02:00
parent b0229b6332
commit 4b7ddcb1c1
14 changed files with 729 additions and 0 deletions
+140
View File
@@ -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