Files
davila7--claude-code-templates/cli-tool/components/agents/obsidian-ops-team/Scripts/metadata_adder.py
T
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08: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()