Make KB Capture project-centric

- Sessions now live inside project folders: <Project>/_capture/<session>/
- Project picker dropdown (scans projects folder)
- Auto-discovers projects with KB/ folder or _context.md
- Windows: D:/ATODrive/Projects
- Linux: ~/obsidian-vault/2-Projects

This aligns with the KB structure where Mario updates:
- KB/dev/gen-XXX.md (session captures)
- Images/screenshot-sessions/ (frames)
This commit is contained in:
Mario Lavoie
2026-02-09 12:53:46 +00:00
parent d5371cfe75
commit 0266fda42b
4 changed files with 199 additions and 68 deletions

View File

@@ -116,27 +116,61 @@ class SessionManager:
"""
Manages recording sessions and clips.
Directory structure:
sessions/
├── <session-id>/
── session.json # Session metadata
│ ├── clips/
│ ├── clip-001.mp4
├── clip-002.mp4
│ │ └── ...
└── export/ # Final export for Clawdbot
├── merged.mp4
├── transcript.json
└── metadata.json
Project-centric structure:
/2-Projects/<ProjectName>/
├── KB/
── dev/ # gen-XXX.md session captures (Mario creates)
├── Images/
└── screenshot-sessions/ # Frames organized by session
└── _capture/ # Session staging
└── <session-id>/
├── session.json
├── clips/
├── clip-001.mp4
└── ...
└── clawdbot_export/ # Ready for Mario
├── merged.mp4
├── transcript.json
└── metadata.json
"""
def __init__(self, base_path: Path):
self.base_path = Path(base_path)
self.sessions_dir = self.base_path / "sessions"
self.sessions_dir.mkdir(parents=True, exist_ok=True)
def __init__(self, projects_root: Path):
"""
Initialize session manager.
Args:
projects_root: Path to projects folder (e.g., /2-Projects/ or D:/ATODrive/Projects/)
"""
self.projects_root = Path(projects_root)
self.current_session: Optional[Session] = None
self.current_clip: Optional[Clip] = None
self._current_project_path: Optional[Path] = None
def list_projects(self) -> List[str]:
"""List available projects (folders in projects_root)."""
projects = []
if self.projects_root.exists():
for p in sorted(self.projects_root.iterdir()):
if p.is_dir() and not p.name.startswith((".", "_")):
# Check if it looks like a project (has KB folder or _context.md)
if (p / "KB").exists() or (p / "_context.md").exists():
projects.append(p.name)
return projects
def get_project_path(self, project: str) -> Path:
"""Get full path to a project."""
return self.projects_root / project
def get_capture_dir(self, project: str) -> Path:
"""Get the _capture directory for a project."""
return self.get_project_path(project) / "_capture"
@property
def sessions_dir(self) -> Path:
"""Current project's capture directory."""
if self._current_project_path:
return self._current_project_path / "_capture"
raise RuntimeError("No project selected")
def start_session(
self,
@@ -144,7 +178,12 @@ class SessionManager:
project: str,
session_type: SessionType = SessionType.DESIGN,
) -> Session:
"""Start a new recording session."""
"""Start a new recording session within a project."""
# Set current project
self._current_project_path = self.get_project_path(project)
if not self._current_project_path.exists():
raise ValueError(f"Project not found: {project}")
session_id = datetime.now().strftime("%Y%m%d-%H%M%S")
session = Session(
@@ -155,8 +194,9 @@ class SessionManager:
created_at=datetime.now(),
)
# Create session directory
session_dir = self.sessions_dir / session_id
# Create session directory in project's _capture folder
capture_dir = self._current_project_path / "_capture"
session_dir = capture_dir / session_id
session_dir.mkdir(parents=True, exist_ok=True)
(session_dir / "clips").mkdir(exist_ok=True)
@@ -299,15 +339,26 @@ class SessionManager:
return Session.from_dict(json.load(f))
return None
def list_sessions(self) -> List[Session]:
"""List all sessions."""
def list_sessions(self, project: Optional[str] = None) -> List[Session]:
"""List sessions, optionally filtered by project."""
sessions = []
for session_dir in sorted(self.sessions_dir.iterdir(), reverse=True):
if session_dir.is_dir():
session_file = session_dir / "session.json"
if session_file.exists():
with open(session_file) as f:
sessions.append(Session.from_dict(json.load(f)))
if project:
# List sessions for specific project
capture_dir = self.get_capture_dir(project)
if capture_dir.exists():
for session_dir in sorted(capture_dir.iterdir(), reverse=True):
if session_dir.is_dir():
session_file = session_dir / "session.json"
if session_file.exists():
with open(session_file) as f:
sessions.append(Session.from_dict(json.load(f)))
else:
# List sessions across all projects
for proj in self.list_projects():
sessions.extend(self.list_sessions(proj))
sessions.sort(key=lambda s: s.created_at, reverse=True)
return sessions
def get_session_dir(self, session_id: str) -> Path: