Files
Daniel Avila 11a0c93715 feat: add support for agent subcategories with dynamic GitHub discovery (#56)
- Support agent installation from subcategories (e.g., deep-research-team/academic-researcher)
- Add dynamic GitHub API integration to discover available agents in real-time
- Automatically organize agents by category folders
- Preserve folder structure when installing categorized agents
- Add comprehensive agent teams from merged PR #46:
  - Deep Research Team (9 specialized research agents)
  - FFmpeg Clip Team (6 media processing agents)
  - MCP Development Team (5 MCP server agents)
  - Obsidian Operations Team (5 knowledge management agents)
  - OCR Extraction Team (5 text processing agents)
  - Podcast Creator Team (9 content creation agents)
  - Plus individual specialized agents

Technical improvements:
- Remove hardcoded agent lists in favor of GitHub repository scanning
- Support both 'agent-name' and 'category/agent-name' formats
- Fallback mechanism when GitHub API is unavailable
- Maintains backward compatibility with existing CLI usage
- Real-time agent discovery without requiring CLI updates

Usage examples:
- cct --agent api-security-audit (existing agents)
- cct --agent deep-research-team/academic-researcher (new categorized agents)
- cct --agent help (shows all available agents dynamically)



🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-01 12:09:18 -04:00

197 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Metadata Adder for Obsidian Vault
Adds standardized frontmatter to markdown files that lack it.
"""
import os
import re
from datetime import datetime
from pathlib import Path
import argparse
class MetadataAdder:
def __init__(self, vault_path):
self.vault_path = Path(vault_path)
self.stats = {
'processed': 0,
'updated': 0,
'skipped': 0,
'errors': 0
}
def get_file_creation_date(self, file_path):
"""Get file creation date from filesystem."""
try:
stat = os.stat(file_path)
# Use birthtime on macOS, ctime on others
timestamp = stat.st_birthtime if hasattr(stat, 'st_birthtime') else stat.st_ctime
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d')
except:
return datetime.now().strftime('%Y-%m-%d')
def determine_file_type(self, file_path):
"""Determine the type of note based on path and content."""
path_str = str(file_path).lower()
if 'moc' in path_str or 'map of content' in path_str:
return 'map-of-content'
elif 'daily notes' in path_str or 'daily note' in path_str:
return 'daily'
elif 'research' in path_str or 'articles' in path_str:
return 'research'
elif 'client' in path_str or 'camrohn llc' in path_str:
return 'client-work'
elif 'tutorial' in path_str or 'course' in path_str:
return 'tutorial'
elif 'idea' in path_str:
return 'idea'
elif 'meeting' in path_str:
return 'meeting'
elif 'email' in path_str:
return 'email'
else:
return 'note'
def generate_tags_from_path(self, file_path):
"""Generate tags based on file path."""
tags = []
path_parts = file_path.relative_to(self.vault_path).parts[:-1] # Exclude filename
# Map directory names to tags
tag_mapping = {
'ai development': 'ai/development',
'ai articles': 'ai/research',
'ai courses': 'tutorial/course',
'ai ideas': 'idea',
'camrohn llc': 'client',
'daily notes': 'daily',
'clippings': 'clippings',
'mcp': 'mcp',
'langchain': 'langchain',
'graphrag': 'graphrag'
}
for part in path_parts:
part_lower = part.lower()
for key, tag in tag_mapping.items():
if key in part_lower:
tags.append(tag)
break
# Add date tags for daily notes
if 'daily' in tags:
created_date = self.get_file_creation_date(file_path)
year_month = datetime.strptime(created_date, '%Y-%m-%d').strftime('%Y/%m')
tags.append(f'daily/{year_month}')
return list(set(tags)) # Remove duplicates
def has_frontmatter(self, content):
"""Check if content already has frontmatter."""
return content.strip().startswith('---')
def create_frontmatter(self, file_path, existing_content):
"""Create appropriate frontmatter for the file."""
created_date = self.get_file_creation_date(file_path)
file_type = self.determine_file_type(file_path)
tags = self.generate_tags_from_path(file_path)
# Extract title from first heading or filename
title_match = re.search(r'^#\s+(.+)$', existing_content, re.MULTILINE)
if title_match:
title = title_match.group(1)
else:
title = file_path.stem.replace('_', ' ').replace('-', ' ')
frontmatter = f"""---
tags: {tags}
type: {file_type}
created: {created_date}
modified: {datetime.now().strftime('%Y-%m-%d')}
status: active
related: []
aliases: []
---
"""
return frontmatter
def process_file(self, file_path):
"""Process a single markdown file."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if self.has_frontmatter(content):
self.stats['skipped'] += 1
return False
# Create and prepend frontmatter
frontmatter = self.create_frontmatter(file_path, content)
new_content = frontmatter + content
# Write back to file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
self.stats['updated'] += 1
return True
except Exception as e:
print(f"Error processing {file_path}: {e}")
self.stats['errors'] += 1
return False
def process_vault(self, dry_run=False):
"""Process all markdown files in the vault."""
# Directories to skip
skip_dirs = {'.obsidian', '.trash', 'System_Files', '.git'}
for file_path in self.vault_path.rglob('*.md'):
# Skip files in excluded directories
if any(skip_dir in file_path.parts for skip_dir in skip_dirs):
continue
self.stats['processed'] += 1
if dry_run:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if not self.has_frontmatter(content):
print(f"Would update: {file_path.relative_to(self.vault_path)}")
self.stats['updated'] += 1
else:
self.stats['skipped'] += 1
else:
if self.process_file(file_path):
print(f"Updated: {file_path.relative_to(self.vault_path)}")
return self.stats
def main():
parser = argparse.ArgumentParser(description='Add metadata to Obsidian vault files')
parser.add_argument('--vault', default='/Users/cam/VAULT01',
help='Path to Obsidian vault')
parser.add_argument('--dry-run', action='store_true',
help='Show what would be updated without making changes')
args = parser.parse_args()
adder = MetadataAdder(args.vault)
print(f"Processing vault at: {args.vault}")
print("Dry run mode" if args.dry_run else "Making changes")
print("-" * 50)
stats = adder.process_vault(dry_run=args.dry_run)
print("-" * 50)
print(f"Files processed: {stats['processed']}")
print(f"Files updated: {stats['updated']}")
print(f"Files skipped (already have metadata): {stats['skipped']}")
print(f"Errors: {stats['errors']}")
if __name__ == '__main__':
main()