chore: import upstream snapshot with attribution
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:58 +08:00
commit bb5c75ce05
8824 changed files with 1946442 additions and 0 deletions
@@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""
Daily Notes Connectivity Agent
Analyzes daily notes and creates meaningful connections between them and other vault content.
"""
import os
import re
import yaml
from datetime import datetime, timedelta
from pathlib import Path
from collections import defaultdict
import json
class DailyNotesConnector:
def __init__(self, vault_path):
self.vault_path = Path(vault_path)
self.connections_made = 0
self.notes_processed = 0
self.patterns = {
'project': r'(?:project|AI IDEAS|idea|experiment|build|develop)',
'meeting': r'(?:meeting|call|discussion|client|consultation)',
'technical': r'(?:MCP|LangChain|GraphRAG|AI|ML|model|agent|tool)',
'client': r'(?:client|consulting|business|CamRohn)',
'personal': r'(?:family|personal|reflection|stoic|goal)',
'research': r'(?:research|paper|study|article|documentation)',
'community': r'(?:Austin|LangChain|meetup|community|conference)'
}
self.connection_map = defaultdict(list)
def find_daily_notes(self):
"""Find all daily notes across the vault."""
daily_notes = []
# Search patterns for daily notes
patterns = [
self.vault_path / "Daily Notes" / "*.md",
self.vault_path / "REMOTE_VAULT01" / "Daily Notes" / "*.md",
self.vault_path / "Daily Email" / "*.md",
self.vault_path / "_PERSONAL_" / "JOURNAL" / "**" / "*.md"
]
for pattern in patterns:
for file_path in self.vault_path.glob(str(pattern).split(str(self.vault_path) + "/")[1]):
# Check if filename matches date pattern
if re.match(r'\d{4}-\d{2}-\d{2}', file_path.stem):
daily_notes.append(file_path)
return sorted(daily_notes)
def extract_frontmatter(self, file_path):
"""Extract frontmatter from a markdown file."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if content.startswith('---'):
try:
end_index = content.index('---', 3)
frontmatter_text = content[3:end_index].strip()
return yaml.safe_load(frontmatter_text), content[end_index+3:]
except:
return {}, content
return {}, content
def update_frontmatter(self, file_path, frontmatter, body):
"""Update the frontmatter of a file."""
yaml_content = yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True)
new_content = f"---\n{yaml_content}---\n{body}"
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
def analyze_content(self, content):
"""Analyze content to identify topics and themes."""
content_lower = content.lower()
topics = defaultdict(int)
for topic, pattern in self.patterns.items():
matches = re.findall(pattern, content_lower)
topics[topic] = len(matches)
# Extract specific mentions
mentions = {
'projects': re.findall(r'\[\[([^]]+)\]\]', content),
'headers': re.findall(r'^#+\s+(.+)$', content, re.MULTILINE),
'urls': re.findall(r'https?://[^\s\]]+', content),
'tags': re.findall(r'#(\w+)', content)
}
return topics, mentions
def find_related_content(self, topics, mentions, current_file):
"""Find related content based on topics and mentions."""
related = []
# Map topics to vault directories
topic_dirs = {
'project': ['AI IDEAS', 'AI Development'],
'meeting': ['CamRohn LLC/Client Work', 'Austin LangChain'],
'technical': ['AI Development', 'Model Context Protocol (MCP)'],
'client': ['CamRohn LLC', 'Second Opinion DDS'],
'research': ['AI Articles and Research', 'Clippings'],
'community': ['Austin LangChain', 'AI Conferences and Competitions']
}
# Find files based on dominant topics
for topic, count in sorted(topics.items(), key=lambda x: x[1], reverse=True):
if count > 0 and topic in topic_dirs:
for dir_name in topic_dirs[topic]:
dir_path = self.vault_path / dir_name
if dir_path.exists():
# Add MOC if exists
moc_path = dir_path / f"MOC - {dir_name.split('/')[-1]}.md"
if moc_path.exists():
related.append((moc_path, f"{topic} reference"))
# Add specific mentioned files
for mention in mentions['projects']:
if dir_name in mention:
file_path = self.vault_path / f"{mention}.md"
if file_path.exists() and file_path != current_file:
related.append((file_path, "direct mention"))
return related[:10] # Limit to top 10 connections
def find_temporal_connections(self, file_path, all_notes):
"""Find temporal connections (previous/next days, weekly summaries)."""
temporal = []
# Extract date from filename
date_match = re.match(r'(\d{4})-(\d{2})-(\d{2})', file_path.stem)
if not date_match:
return temporal
current_date = datetime(int(date_match.group(1)),
int(date_match.group(2)),
int(date_match.group(3)))
# Find previous and next days
for days_offset in [-1, 1]:
target_date = current_date + timedelta(days=days_offset)
target_str = target_date.strftime('%Y-%m-%d')
for note in all_notes:
if target_str in note.stem:
temporal.append((note, f"{'Previous' if days_offset < 0 else 'Next'} day"))
break
# Find weekly connections (same week)
week_start = current_date - timedelta(days=current_date.weekday())
week_end = week_start + timedelta(days=6)
for note in all_notes:
date_match = re.match(r'(\d{4})-(\d{2})-(\d{2})', note.stem)
if date_match:
note_date = datetime(int(date_match.group(1)),
int(date_match.group(2)),
int(date_match.group(3)))
if week_start <= note_date <= week_end and note != file_path:
temporal.append((note, "Same week"))
return temporal
def process_daily_note(self, file_path, all_notes):
"""Process a single daily note and add connections."""
print(f"Processing: {file_path.relative_to(self.vault_path)}")
frontmatter, body = self.extract_frontmatter(file_path)
topics, mentions = self.analyze_content(body)
# Find related content
content_related = self.find_related_content(topics, mentions, file_path)
temporal_related = self.find_temporal_connections(file_path, all_notes)
# Build related list
new_related = []
# Add temporal connections first
for related_file, relation_type in temporal_related:
if "Previous" in relation_type or "Next" in relation_type:
relative_path = related_file.relative_to(self.vault_path)
link = f"[[{relative_path.with_suffix('').as_posix()}]]"
if relation_type == "Previous day":
new_related.insert(0, f"{link} # {relation_type}")
else:
new_related.append(f"{link} # {relation_type}")
# Add content-based connections
for related_file, relation_type in content_related:
relative_path = related_file.relative_to(self.vault_path)
link = f"[[{relative_path.with_suffix('').as_posix()}]]"
comment = f" # {relation_type.title()}"
new_related.append(f"{link}{comment}")
# Update frontmatter if we found new connections
if new_related:
existing_related = frontmatter.get('related', [])
if isinstance(existing_related, list):
# Merge and deduplicate - convert lists to strings for deduplication
combined = existing_related + new_related
seen = set()
all_related = []
for item in combined:
if item not in seen:
seen.add(item)
all_related.append(item)
else:
all_related = new_related
frontmatter['related'] = all_related
self.update_frontmatter(file_path, frontmatter, body)
self.connections_made += len(new_related)
self.notes_processed += 1
# Track patterns for reporting
for topic, count in topics.items():
if count > 0:
self.connection_map[topic].append(file_path.stem)
def generate_report(self):
"""Generate a report of connections made."""
report = f"""# Daily Notes Connectivity Report
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}
## Summary
- Total daily notes processed: {self.notes_processed}
- Total connections created: {self.connections_made}
- Average connections per note: {self.connections_made / max(self.notes_processed, 1):.1f}
## Connection Patterns Discovered
"""
for topic, dates in self.connection_map.items():
if dates:
report += f"### {topic.title()} Topics\n"
report += f"Found in {len(dates)} daily notes:\n"
# Show recent examples
for date in sorted(dates)[-5:]:
report += f"- [[{date}]]\n"
report += "\n"
report += """## Themes Across Time Periods
### Recent Trends (Last 30 days)
"""
# Analyze recent trends
recent_date = datetime.now() - timedelta(days=30)
recent_topics = defaultdict(int)
for topic, dates in self.connection_map.items():
for date_str in dates:
try:
date_match = re.match(r'(\d{4})-(\d{2})-(\d{2})', date_str)
if date_match:
note_date = datetime(int(date_match.group(1)),
int(date_match.group(2)),
int(date_match.group(3)))
if note_date >= recent_date:
recent_topics[topic] += 1
except:
pass
for topic, count in sorted(recent_topics.items(), key=lambda x: x[1], reverse=True):
report += f"- **{topic.title()}**: {count} occurrences\n"
report += "\n## Recommendations\n\n"
report += "1. Consider creating weekly/monthly summary notes to consolidate themes\n"
report += "2. Review orphaned daily notes that lack connections\n"
report += "3. Add more content to empty daily notes for better connectivity\n"
return report
def run(self):
"""Main execution method."""
print("Daily Notes Connectivity Agent Starting...")
print(f"Vault path: {self.vault_path}")
# Find all daily notes
daily_notes = self.find_daily_notes()
print(f"Found {len(daily_notes)} daily notes")
# Process each note
for note in daily_notes:
try:
self.process_daily_note(note, daily_notes)
except Exception as e:
print(f"Error processing {note}: {e}")
# Generate and save report
report = self.generate_report()
report_path = self.vault_path / "System_Files" / "Daily_Notes_Connectivity_Report.md"
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
print(f"\nComplete! Report saved to: {report_path}")
print(f"Processed {self.notes_processed} notes, created {self.connections_made} connections")
if __name__ == "__main__":
vault_path = "/Users/cam/VAULT01"
connector = DailyNotesConnector(vault_path)
connector.run()
@@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""
Enhanced Tag Standardizer for hierarchical tag structure.
Consolidates similar tags and applies hierarchical organization.
"""
import os
import re
import yaml
from pathlib import Path
from collections import defaultdict, Counter
class EnhancedTagStandardizer:
def __init__(self, vault_path):
self.vault_path = Path(vault_path)
self.files_updated = 0
# Enhanced mappings for better consolidation
self.enhanced_mappings = {
# AI hierarchy consolidation
'ai-development': 'ai/development',
'ai-ideas': 'idea', # Ideas are tracked with 'idea' tag
'ai-tools': 'ai/tools',
'ai-consulting': 'consulting',
'ai-courses': 'learning/course',
'ai-conferences-and-competitions': 'learning/conference',
'ai-articles-and-research': 'ai/research',
'ai-agent': 'ai/agents',
'ai-community': 'community',
'ai-integration': 'ai/development',
'ai-services': 'ai/tools',
# Business hierarchy
'business-strategy': 'business/strategy',
'business-development': 'business/development',
'business-intelligence': 'business/analytics',
'business-model': 'business/strategy',
'business-automation': 'automation',
'business-systems': 'business/systems',
'business-case': 'business/strategy',
'business-context': 'business',
'business-research': 'business/research',
'business-mapping': 'business/strategy',
'business-report': 'business/analytics',
'business-operations': 'business/operations',
'business-plan': 'business/strategy',
'business-transformation': 'business/strategy',
'business-solutions': 'business/solutions',
'business-assets': 'business/assets',
# Client work standardization
'client-work': 'client',
'client-materials': 'client',
'client-analytics': 'client',
'client-communication': 'client',
# Learning hierarchy
'tutorial/course': 'learning/course',
'learning-paths': 'learning',
'courses': 'learning/course',
'tutorials': 'tutorial',
'guides': 'tutorial',
'training': 'learning',
'certifications': 'learning/certification',
# Technical tags
'development-tools': 'ai/tools',
'tools': 'ai/tools',
'apis': 'api',
'api-integration': 'api',
'api-testing': 'testing',
# Content organization
'daily-notes': 'daily',
'daily-email': 'email',
'email-summary': 'email',
'email-processing': 'email',
'email-marketing': 'marketing',
# Web development
'web-development': 'development',
'web-presence': 'business/web-presence',
'_web-presence-development': 'business/web-presence',
# Personal tags
'_personal_': 'personal',
'personal-development': 'personal/development',
# Project management
'project-management': 'project',
'project-timeline': 'project',
# Marketing & content
'content-strategy': 'marketing/content',
'content-marketing': 'marketing/content',
'content-calendar': 'marketing/calendar',
'content-distribution': 'marketing/distribution',
'marketing-strategy': 'marketing/strategy',
# Technical infrastructure
'it-infrastructure': 'infrastructure',
'server-management': 'infrastructure',
'server-setup': 'infrastructure',
# Data and analytics
'data-processing': 'data',
'data-sources': 'data',
'data-security': 'security',
# Remove underscores from tags
'_tutorials_': 'tutorial',
'_business-formation_': 'business/formation',
# Standardize compound words
'meeting-notes': 'meeting',
'meetings': 'meeting',
# Standardize plural forms
'agents': 'ai/agents',
'templates': 'template',
'projects': 'project',
'tasks': 'action/todo',
'sources': 'source',
'systems': 'system',
'solutions': 'solution',
'recommendations': 'recommendation',
'transcripts': 'transcript',
'discussions': 'discussion',
'platforms': 'platform',
'frameworks': 'framework',
'pipelines': 'pipeline',
'servers': 'server',
'summaries': 'summary',
'conferences': 'conference',
'opportunities': 'opportunity',
'datasets': 'dataset',
# Consolidate similar concepts
'thought-leadership': 'authority-building',
'technical-authority': 'authority-building',
'brainstorming': 'idea',
'strategic-planning': 'strategy',
'strategic-decision-making': 'strategy',
'strategic-overview': 'strategy',
'strategic-connections': 'strategy',
# Standardize vendor/tool names
'anthropic_blog': 'anthropic',
'anthropic_github': 'anthropic',
'github_topic_-_mcp': 'mcp',
'github_topic_-_mcp_server': 'mcp',
'mcp_reddit': 'mcp',
'mcp_documentation': 'mcp',
'mcp_github_discussions': 'mcp',
'mcp-server': 'mcp',
'npm_-_mcp_packages': 'mcp',
'dev.to_mcp_tag': 'mcp',
'medium_-_mcp_topics': 'mcp',
'pulsemcp_blog': 'pulsemcp',
# Clean up system tags
'system_files': 'system',
'remote_vault01': 'remote-sync',
# Consolidate database tags
'graph-db': 'database',
'graph-databases': 'database',
'vector-databases': 'database',
'database-queries': 'database',
'database-updates': 'database',
# Consolidate AI concepts
'ai/rag': 'ai/embeddings',
'agentic-rag': 'ai/embeddings',
'knowledge-graph': 'graphrag',
'knowledge-network': 'graphrag',
'knowledge-management': 'knowledge-base',
# Family tags
'family-projects': 'family/projects',
'family/index': 'family',
# Visual content
'visual-assets': 'visual-assets',
'visual-organization': 'visual-assets',
'visual-learning': 'visual-assets',
'visual-search': 'visual-assets',
'image-gallery': 'gallery',
'image-generation': 'ai/tools',
'screenshots': 'visual-assets',
'screenshots-and-references': 'visual-assets',
'infographics': 'visual-assets',
'charts': 'visual-assets',
'images': 'visual-assets',
'snagit-captures': 'visual-assets',
# Analytics and metrics
'analytics': 'analytics',
'client-analytics': 'analytics',
'revenue-analytics': 'analytics',
'performance-metrics': 'analytics',
'financial-analysis': 'finance',
'roi-analysis': 'roi',
'roi-calculator': 'roi',
# Simplify compound concepts
'complexity-analysis': 'analysis',
'connection-analysis': 'analysis',
'network-analysis': 'analysis',
'schema-analysis': 'analysis',
'competitive-intelligence': 'analysis',
'competitor-tracking': 'analysis',
# Standardize workflow tags
'workflow': 'workflows',
# Clean up misc tags
'--ollama-deep-research': 'ollama',
'second-opinion-dds': 'dental',
'austin-langchain': 'community',
'the-build': 'build',
'mcpcentral.io': 'mcpcentral',
}
def extract_frontmatter(self, content):
"""Extract YAML frontmatter from content."""
if not content.strip().startswith('---'):
return None, content
lines = content.split('\n')
if len(lines) < 3:
return None, content
end_idx = None
for i in range(1, len(lines)):
if lines[i].strip() == '---':
end_idx = i
break
if end_idx is None:
return None, content
try:
frontmatter_text = '\n'.join(lines[1:end_idx])
frontmatter = yaml.safe_load(frontmatter_text)
remaining_content = '\n'.join(lines[end_idx + 1:])
return frontmatter, remaining_content
except yaml.YAMLError:
return None, content
def enhance_tag(self, tag):
"""Apply enhanced tag standardization."""
# Remove leading/trailing whitespace
tag = tag.strip()
# Remove hash if present
if tag.startswith('#'):
tag = tag[1:]
# Apply enhanced mappings
if tag in self.enhanced_mappings:
return self.enhanced_mappings[tag]
# Default: lowercase and replace spaces with hyphens
return tag.lower().replace(' ', '-')
def process_file(self, file_path):
"""Process a single file with enhanced tag standardization."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
frontmatter, remaining_content = self.extract_frontmatter(content)
if frontmatter is None or 'tags' not in frontmatter:
return False
# Get current tags
current_tags = frontmatter.get('tags', [])
if not current_tags:
return False
# Standardize tags
if isinstance(current_tags, list):
new_tags = []
changed = False
for tag in current_tags:
if isinstance(tag, str):
enhanced = self.enhance_tag(tag)
if enhanced != tag:
changed = True
if enhanced and enhanced not in new_tags:
new_tags.append(enhanced)
if changed:
# Update frontmatter
frontmatter['tags'] = new_tags
# Reconstruct content
new_frontmatter = yaml.dump(frontmatter, default_flow_style=False, sort_keys=False)
new_content = f"---\n{new_frontmatter}---\n{remaining_content}"
# Write back to file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Enhanced: {file_path.relative_to(self.vault_path)}")
return True
return False
except Exception as e:
print(f"Error processing {file_path}: {e}")
return False
def process_vault(self):
"""Process all files in the vault."""
skip_dirs = {'.obsidian', '.trash', '.git'}
for file_path in self.vault_path.rglob('*.md'):
if any(skip_dir in file_path.parts for skip_dir in skip_dirs):
continue
if self.process_file(file_path):
self.files_updated += 1
return self.files_updated
def main():
vault_path = '/Users/cam/VAULT01'
print(f"Enhanced tag standardization for: {vault_path}")
print("This will consolidate similar tags and apply hierarchical organization")
print("-" * 60)
standardizer = EnhancedTagStandardizer(vault_path)
updated = standardizer.process_vault()
print("-" * 60)
print(f"Total files updated: {updated}")
if __name__ == '__main__':
main()
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Find and implement keyword-based connections between files."""
import os
import re
from collections import Counter, defaultdict
from pathlib import Path
# Priority keywords to focus on
PRIORITY_KEYWORDS = {
'llm', 'langchain', 'langgraph', 'rag', 'embedding', 'vector',
'agent', 'automation', 'workflow', 'pipeline', 'mcp', 'api',
'anthropic', 'openai', 'google', 'claude', 'gpt', 'model',
'context', 'protocol', 'fastapi', 'docker', 'cloudflare',
'supabase', 'integration', 'framework', 'retrieval', 'augmented',
'generation', 'prompt', 'engineering', 'multimodal', 'function',
'calling', 'tool', 'use', 'chain', 'thought', 'reasoning'
}
def extract_keywords_from_file(file_path):
"""Extract meaningful keywords from a file."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read().lower()
# Extract words (alphanumeric plus hyphens)
words = re.findall(r'\b[a-z0-9-]+\b', content)
# Filter for priority keywords and count occurrences
keyword_counts = Counter()
for word in words:
if word in PRIORITY_KEYWORDS:
keyword_counts[word] += 1
return keyword_counts
except:
return Counter()
def find_keyword_connections(vault_root):
"""Find files that share multiple priority keywords."""
search_dirs = [
vault_root / "AI Development",
vault_root / "AI Articles and Research",
vault_root / "AI IDEAS",
vault_root / "AI Courses",
vault_root / "CamRohn LLC",
vault_root / "Clippings"
]
# Collect keyword data for all files
file_keywords = {}
print("Analyzing files for keywords...")
for search_dir in search_dirs:
if not search_dir.exists():
continue
for root, dirs, files in os.walk(search_dir):
for file in files:
if file.endswith('.md'):
file_path = Path(root) / file
keywords = extract_keywords_from_file(file_path)
if keywords:
file_keywords[file_path] = keywords
print(f"Analyzed {len(file_keywords)} files")
# Find connections between files
connections = []
processed_pairs = set()
files = list(file_keywords.keys())
for i, file1 in enumerate(files):
if i % 100 == 0:
print(f"Processing file {i+1}/{len(files)}...")
for j, file2 in enumerate(files[i+1:], start=i+1):
# Skip if already processed
pair = tuple(sorted([str(file1), str(file2)]))
if pair in processed_pairs:
continue
processed_pairs.add(pair)
# Find common keywords
common_keywords = set(file_keywords[file1].keys()) & set(file_keywords[file2].keys())
if len(common_keywords) >= 5: # At least 5 common keywords
# Calculate relevance score
score = sum(
min(file_keywords[file1][kw], file_keywords[file2][kw])
for kw in common_keywords
)
connections.append({
'file1': file1,
'file2': file2,
'keywords': list(common_keywords),
'score': score
})
# Sort by score
connections.sort(key=lambda x: x['score'], reverse=True)
return connections
def add_link_to_file(file_path, link_to_add, link_text=None):
"""Add a link to a file if it doesn't already exist."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract just the filename for the link
link_filename = Path(link_to_add).stem
# Check if link already exists
if f'[[{link_filename}]]' in content:
return False
# Find or create Related section
related_match = re.search(r'^## Related\s*$', content, re.MULTILINE)
if related_match:
# Insert after the Related header
insert_pos = related_match.end()
# Skip to next line
next_line = content.find('\n', insert_pos)
if next_line != -1:
insert_pos = next_line + 1
else:
# Add Related section at end
if not content.endswith('\n'):
content += '\n'
content += '\n## Related\n'
insert_pos = len(content)
# Create the link line
if link_text:
link_line = f"- [[{link_filename}]] - {link_text}\n"
else:
link_line = f"- [[{link_filename}]]\n"
# Insert the link
new_content = content[:insert_pos] + link_line + content[insert_pos:]
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
return True
except Exception as e:
print(f"Error updating {file_path}: {e}")
return False
def implement_keyword_connections(connections, limit=50):
"""Implement the top keyword-based connections."""
connections_made = 0
keyword_clusters = defaultdict(list)
for conn in connections[:limit]:
file1 = conn['file1']
file2 = conn['file2']
keywords = conn['keywords']
# Create descriptive text
keyword_text = f"Shares keywords: {', '.join(sorted(keywords)[:5])}"
# Add bidirectional links
if add_link_to_file(file1, file2, keyword_text):
connections_made += 1
print(f"Connected {file1.name}{file2.name}")
# Track keyword clusters
for kw in keywords:
keyword_clusters[kw].append((file1.name, file2.name))
if add_link_to_file(file2, file1, keyword_text):
connections_made += 1
return connections_made, keyword_clusters
def main():
vault_root = Path("/Users/cam/VAULT01")
print("Finding keyword-based connections...")
connections = find_keyword_connections(vault_root)
print(f"\nFound {len(connections)} potential keyword connections")
# Show top connections
print("\nTop 10 keyword connections by relevance:")
for i, conn in enumerate(connections[:10]):
print(f"{i+1}. {conn['file1'].name}{conn['file2'].name}")
print(f" Keywords ({len(conn['keywords'])}): {', '.join(sorted(conn['keywords'])[:8])}")
print(f" Score: {conn['score']}")
# Implement connections
print("\nImplementing connections...")
connections_made, keyword_clusters = implement_keyword_connections(connections)
print(f"\n=== Keyword Connection Report ===")
print(f"Total connections made: {connections_made}")
print("\nMost connected keywords:")
sorted_keywords = sorted(keyword_clusters.items(), key=lambda x: len(x[1]), reverse=True)[:10]
for keyword, connections in sorted_keywords:
print(f" {keyword}: {len(connections)} connections")
# Find cross-domain connections
print("\nCross-domain connections:")
cross_domain = 0
for i, (keyword, conn_list) in enumerate(sorted_keywords[:5]):
print(f"\n{keyword}-related cross-domain connections:")
for file1_name, file2_name in conn_list[:3]: # Show first 3 for each keyword
print(f" - {file1_name}{file2_name}")
if __name__ == "__main__":
main()
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Fix quoted tags in frontmatter by converting single-quoted tags to unquoted format.
"""
import os
import re
import yaml
from pathlib import Path
def fix_quoted_tags(vault_path):
"""Fix single-quoted tags in all markdown files."""
vault_path = Path(vault_path)
files_updated = 0
for file_path in vault_path.rglob('*.md'):
if any(skip in file_path.parts for skip in ['.obsidian', '.trash', '.git']):
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check if file has frontmatter
if not content.strip().startswith('---'):
continue
# Find frontmatter boundaries
lines = content.split('\n')
if len(lines) < 3:
continue
end_idx = None
for i in range(1, len(lines)):
if lines[i].strip() == '---':
end_idx = i
break
if end_idx is None:
continue
# Extract frontmatter
frontmatter_text = '\n'.join(lines[1:end_idx])
# Check if tags line has quoted values
if re.search(r"tags:\s*\[['\"]", frontmatter_text):
# Parse frontmatter
try:
frontmatter = yaml.safe_load(frontmatter_text)
if frontmatter and 'tags' in frontmatter:
# Update tags (remove quotes)
tags = frontmatter['tags']
if isinstance(tags, list):
# Tags are already in list format, just ensure they're not quoted
frontmatter['tags'] = [str(tag) for tag in tags]
# Reconstruct content
new_frontmatter = yaml.dump(frontmatter, default_flow_style=False, sort_keys=False)
remaining_content = '\n'.join(lines[end_idx + 1:])
new_content = f"---\n{new_frontmatter}---\n{remaining_content}"
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
files_updated += 1
print(f"Fixed: {file_path.relative_to(vault_path)}")
except yaml.YAMLError as e:
print(f"YAML error in {file_path}: {e}")
except Exception as e:
print(f"Error processing {file_path}: {e}")
return files_updated
if __name__ == '__main__':
vault_path = '/Users/cam/VAULT01'
print(f"Fixing quoted tags in: {vault_path}")
updated = fix_quoted_tags(vault_path)
print(f"\nTotal files updated: {updated}")
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""Implement entity-based connections from Link Suggestions Report."""
import re
import os
from pathlib import Path
# Priority entities to focus on
PRIORITY_ENTITIES = {
'langchain', 'langgraph', 'llm', 'rag', 'embedding', 'vector',
'mcp', 'model context protocol', 'api integration', 'function calling',
'anthropic', 'openai', 'google', 'claude', 'gpt',
'autonomous agent', 'ai agent', 'chain of thought', 'prompt engineering',
'retrieval augmented', 'graphrag', 'multimodal', 'tool use'
}
def read_file(file_path):
"""Read file content."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except:
return None
def write_file(file_path, content):
"""Write content to file."""
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return True
except:
return False
def find_file(filename, search_dirs):
"""Find a file in the vault."""
for dir_path in search_dirs:
for root, dirs, files in os.walk(dir_path):
for file in files:
if file == filename or file == filename + '.md':
return os.path.join(root, file)
return None
def add_link_to_file(file_path, link_to_add, link_text=None):
"""Add a link to a file if it doesn't already exist."""
content = read_file(file_path)
if not content:
return False
# Check if link already exists
if f'[[{link_to_add}]]' in content:
return False
# Find a good place to add the link
# Look for existing "Related:" or "See also:" sections
related_patterns = [
r'^#+\s*Related.*?$',
r'^#+\s*See also.*?$',
r'^#+\s*Links.*?$',
r'^#+\s*References.*?$'
]
insert_pos = None
for pattern in related_patterns:
match = re.search(pattern, content, re.MULTILINE | re.IGNORECASE)
if match:
# Find the end of this section
insert_pos = match.end()
# Skip to next line
next_line = content.find('\n', insert_pos)
if next_line != -1:
insert_pos = next_line + 1
break
# If no related section found, add at the end of frontmatter
if insert_pos is None:
# Find end of frontmatter
frontmatter_end = content.find('---', 3)
if frontmatter_end != -1:
insert_pos = content.find('\n', frontmatter_end + 3) + 1
else:
# No frontmatter, add at beginning
insert_pos = 0
# Create the link line
if link_text:
link_line = f"- [[{link_to_add}]] - {link_text}\n"
else:
link_line = f"- [[{link_to_add}]]\n"
# If we're not in a list section, create one
if insert_pos is None or (insert_pos > 0 and content[insert_pos-2:insert_pos] != '\n\n'):
# Check if we need to add a Related section
needs_section = True
for pattern in related_patterns:
if re.search(pattern, content, re.MULTILINE | re.IGNORECASE):
needs_section = False
break
if needs_section:
# Add at end of file with new Related section
if not content.endswith('\n'):
content += '\n'
content += '\n## Related\n'
content += link_line
else:
# Insert in existing section
new_content = content[:insert_pos] + link_line + content[insert_pos:]
content = new_content
else:
# Insert at found position
new_content = content[:insert_pos] + link_line + content[insert_pos:]
content = new_content
return write_file(file_path, content)
def implement_connections():
"""Implement priority entity connections."""
vault_root = Path("/Users/cam/VAULT01")
search_dirs = [
vault_root / "AI Development",
vault_root / "AI Articles and Research",
vault_root / "AI IDEAS",
vault_root / "AI Courses",
vault_root / "CamRohn LLC",
vault_root / "Clippings"
]
# Read the report
report_path = vault_root / "System_Files" / "Link_Suggestions_Report.md"
report_content = read_file(report_path)
if not report_content:
print("Could not read report")
return
# Parse entity connections
connections_made = 0
connections_by_entity = {}
# Find entity sections
entity_pattern = r'^### (.+)$'
connection_pattern = r'- \[\[([^\]]+)\]\] ↔ \[\[([^\]]+)\]\]'
current_entity = None
for line in report_content.split('\n'):
# Check for entity header
entity_match = re.match(entity_pattern, line)
if entity_match:
current_entity = entity_match.group(1).strip().lower()
continue
# Check for connection
if current_entity and current_entity in [e.lower() for e in PRIORITY_ENTITIES]:
conn_match = re.match(connection_pattern, line)
if conn_match:
file1 = conn_match.group(1).strip()
file2 = conn_match.group(2).strip()
# Skip self-connections
if file1 == file2:
continue
# Find actual file paths
file1_path = find_file(file1 + '.md', search_dirs)
file2_path = find_file(file2 + '.md', search_dirs)
if file1_path and file2_path:
# Add bidirectional links
if add_link_to_file(file1_path, file2, f"Related to {current_entity}"):
connections_made += 1
if current_entity not in connections_by_entity:
connections_by_entity[current_entity] = 0
connections_by_entity[current_entity] += 1
print(f"Added link from {file1} to {file2}")
if add_link_to_file(file2_path, file1, f"Related to {current_entity}"):
connections_made += 1
if current_entity not in connections_by_entity:
connections_by_entity[current_entity] = 0
connections_by_entity[current_entity] += 1
print(f"Added link from {file2} to {file1}")
# Generate report
print("\n=== Connection Implementation Report ===")
print(f"Total connections made: {connections_made}")
print("\nConnections by entity:")
for entity, count in sorted(connections_by_entity.items(), key=lambda x: x[1], reverse=True):
print(f" {entity}: {count} connections")
# Find cross-domain connections
print("\nCross-domain connections created:")
# This would require more complex analysis of file paths
if __name__ == "__main__":
implement_connections()
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""
Link Suggester for Obsidian Vault
Identifies potential connections between notes based on content analysis.
"""
import os
import re
from pathlib import Path
from collections import defaultdict, Counter
import argparse
import json
class LinkSuggester:
def __init__(self, vault_path):
self.vault_path = Path(vault_path)
self.notes = {}
self.entity_mentions = defaultdict(set)
self.potential_links = []
# Common entities to look for
self.entities = {
'technologies': [
'langchain', 'langgraph', 'mcp', 'model context protocol',
'graphrag', 'openai', 'anthropic', 'claude', 'gpt', 'llm',
'ollama', 'huggingface', 'github', 'python', 'javascript',
'cloudflare', 'supabase', 'vector database', 'embedding',
'ai agent', 'autonomous agent', 'rag', 'retrieval augmented'
],
'concepts': [
'machine learning', 'deep learning', 'neural network',
'transformer', 'attention mechanism', 'fine-tuning',
'prompt engineering', 'chain of thought', 'reasoning',
'multimodal', 'text generation', 'code generation',
'tool use', 'function calling', 'api integration'
],
'companies': [
'google', 'microsoft', 'amazon', 'meta', 'apple',
'nvidia', 'intel', 'amd', 'tesla', 'stripe',
'y combinator', 'techstars', 'propel', 'dental'
],
'people': [
'andrew ng', 'geoffrey hinton', 'yann lecun', 'ilya sutskever',
'sam altman', 'dario amodei', 'demis hassabis', 'jensen huang'
]
}
# Flatten entities for easier searching
self.all_entities = []
for category, entities in self.entities.items():
self.all_entities.extend(entities)
def load_notes(self):
"""Load all markdown files and their content."""
skip_dirs = {'.obsidian', '.trash', 'System_Files', '.git'}
for file_path in self.vault_path.rglob('*.md'):
if any(skip_dir in file_path.parts for skip_dir in skip_dirs):
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract title
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
title = title_match.group(1) if title_match else file_path.stem
# Extract existing links
existing_links = set(re.findall(r'\[\[([^\]]+)\]\]', content))
self.notes[file_path] = {
'title': title,
'content': content.lower(),
'existing_links': existing_links,
'word_count': len(content.split())
}
except Exception as e:
print(f"Error reading {file_path}: {e}")
def find_entity_mentions(self):
"""Find mentions of entities across all notes."""
for file_path, note_data in self.notes.items():
content = note_data['content']
for entity in self.all_entities:
if entity in content:
self.entity_mentions[entity].add(file_path)
def suggest_links_by_entities(self):
"""Suggest links based on common entity mentions."""
suggestions = []
for entity, files in self.entity_mentions.items():
if len(files) >= 2: # Entity mentioned in at least 2 files
file_list = list(files)
for i, file1 in enumerate(file_list):
for file2 in file_list[i+1:]:
# Check if files don't already link to each other
note1 = self.notes[file1]
note2 = self.notes[file2]
if (note2['title'] not in note1['existing_links'] and
note1['title'] not in note2['existing_links']):
suggestions.append({
'file1': file1,
'file2': file2,
'title1': note1['title'],
'title2': note2['title'],
'common_entity': entity,
'type': 'entity_mention',
'confidence': len(files) / 10 # Simple confidence score
})
return suggestions
def suggest_links_by_keywords(self):
"""Suggest links based on keyword overlap."""
suggestions = []
# Extract keywords from titles and content
for file_path, note_data in self.notes.items():
if note_data['word_count'] < 100: # Skip very short notes
continue
# Get keywords from title
title_words = set(re.findall(r'\b\w{4,}\b', note_data['title'].lower()))
# Find other notes with similar keywords
for other_path, other_data in self.notes.items():
if file_path == other_path:
continue
other_title_words = set(re.findall(r'\b\w{4,}\b', other_data['title'].lower()))
# Check for keyword overlap
common_words = title_words.intersection(other_title_words)
if len(common_words) >= 2: # At least 2 common significant words
# Check if files don't already link to each other
if (other_data['title'] not in note_data['existing_links'] and
note_data['title'] not in other_data['existing_links']):
suggestions.append({
'file1': file_path,
'file2': other_path,
'title1': note_data['title'],
'title2': other_data['title'],
'common_words': list(common_words),
'type': 'keyword_overlap',
'confidence': len(common_words) / 5
})
return suggestions
def find_orphaned_notes(self):
"""Find notes with no incoming or outgoing links."""
orphaned = []
for file_path, note_data in self.notes.items():
if len(note_data['existing_links']) == 0:
# Check if any other notes link to this one
mentioned_in = []
for other_path, other_data in self.notes.items():
if note_data['title'] in other_data['existing_links']:
mentioned_in.append(other_path)
if not mentioned_in:
orphaned.append({
'file': file_path,
'title': note_data['title'],
'word_count': note_data['word_count']
})
return orphaned
def analyze_vault(self):
"""Perform complete analysis of the vault."""
print("Loading notes...")
self.load_notes()
print(f"Loaded {len(self.notes)} notes")
print("Finding entity mentions...")
self.find_entity_mentions()
print("Generating link suggestions...")
entity_suggestions = self.suggest_links_by_entities()
keyword_suggestions = self.suggest_links_by_keywords()
orphaned_notes = self.find_orphaned_notes()
return {
'entity_suggestions': entity_suggestions,
'keyword_suggestions': keyword_suggestions,
'orphaned_notes': orphaned_notes,
'stats': {
'total_notes': len(self.notes),
'entity_suggestions': len(entity_suggestions),
'keyword_suggestions': len(keyword_suggestions),
'orphaned_notes': len(orphaned_notes)
}
}
def generate_report(self, results, output_file=None):
"""Generate a human-readable report."""
report = []
report.append("# Link Suggestions Report")
report.append(f"Generated for vault: {self.vault_path}")
report.append(f"Total notes analyzed: {results['stats']['total_notes']}")
report.append("")
# Entity-based suggestions
report.append("## Entity-Based Link Suggestions")
report.append(f"Found {len(results['entity_suggestions'])} potential connections")
report.append("")
# Group by entity
entity_groups = defaultdict(list)
for suggestion in results['entity_suggestions']:
entity_groups[suggestion['common_entity']].append(suggestion)
for entity, suggestions in sorted(entity_groups.items()):
report.append(f"### {entity.title()}")
for suggestion in suggestions[:5]: # Top 5 per entity
report.append(f"- [[{suggestion['title1']}]] ↔ [[{suggestion['title2']}]]")
report.append("")
# Keyword-based suggestions
report.append("## Keyword-Based Link Suggestions")
report.append(f"Found {len(results['keyword_suggestions'])} potential connections")
report.append("")
# Sort by confidence
sorted_keywords = sorted(results['keyword_suggestions'],
key=lambda x: x['confidence'], reverse=True)
for suggestion in sorted_keywords[:20]: # Top 20
common_words = ', '.join(suggestion['common_words'])
report.append(f"- [[{suggestion['title1']}]] ↔ [[{suggestion['title2']}]]")
report.append(f" Common words: {common_words}")
report.append("")
# Orphaned notes
report.append("## Orphaned Notes (No Links)")
report.append(f"Found {len(results['orphaned_notes'])} notes with no connections")
report.append("")
# Sort by word count (longer notes first)
sorted_orphaned = sorted(results['orphaned_notes'],
key=lambda x: x['word_count'], reverse=True)
for note in sorted_orphaned[:30]: # Top 30
report.append(f"- [[{note['title']}]] ({note['word_count']} words)")
report_text = '\n'.join(report)
if output_file:
with open(output_file, 'w', encoding='utf-8') as f:
f.write(report_text)
print(f"Report saved to: {output_file}")
return report_text
def main():
parser = argparse.ArgumentParser(description='Suggest links for Obsidian vault')
parser.add_argument('--vault', default='/Users/cam/VAULT01',
help='Path to Obsidian vault')
parser.add_argument('--output',
default='/Users/cam/VAULT01/System_Files/Link_Suggestions_Report.md',
help='Output file for report')
parser.add_argument('--json',
help='Output JSON file for programmatic use')
args = parser.parse_args()
suggester = LinkSuggester(args.vault)
results = suggester.analyze_vault()
# Generate report
report = suggester.generate_report(results, args.output)
# Save JSON if requested
if args.json:
with open(args.json, 'w') as f:
json.dump(results, f, indent=2, default=str)
print(f"JSON data saved to: {args.json}")
# Print summary
print("\n" + "="*50)
print("LINK SUGGESTIONS SUMMARY")
print("="*50)
print(f"Total notes: {results['stats']['total_notes']}")
print(f"Entity-based suggestions: {results['stats']['entity_suggestions']}")
print(f"Keyword-based suggestions: {results['stats']['keyword_suggestions']}")
print(f"Orphaned notes: {results['stats']['orphaned_notes']}")
if __name__ == '__main__':
main()
@@ -0,0 +1,197 @@
#!/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()
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""
MOC (Map of Content) Generator for Obsidian Vault
Automatically generates MOCs for directories and topics.
"""
import os
import re
from pathlib import Path
from datetime import datetime
from collections import defaultdict
import argparse
class MOCGenerator:
def __init__(self, vault_path):
self.vault_path = Path(vault_path)
self.directory_stats = {}
self.topic_clusters = defaultdict(list)
def analyze_directory(self, directory_path):
"""Analyze a directory and its contents."""
stats = {
'total_files': 0,
'md_files': 0,
'subdirectories': [],
'file_types': defaultdict(int),
'common_topics': defaultdict(int)
}
try:
for item in directory_path.iterdir():
if item.is_file():
stats['total_files'] += 1
if item.suffix == '.md':
stats['md_files'] += 1
# Extract topics from filename
topics = self.extract_topics_from_filename(item.name)
for topic in topics:
stats['common_topics'][topic] += 1
stats['file_types'][item.suffix] += 1
elif item.is_dir() and not item.name.startswith('.'):
stats['subdirectories'].append(item.name)
except PermissionError:
print(f"Permission denied: {directory_path}")
return stats
def extract_topics_from_filename(self, filename):
"""Extract potential topics from filename."""
# Remove extension and common prefixes
name = filename.replace('.md', '')
name = re.sub(r'^\d{4}-\d{2}-\d{2}[_-]', '', name) # Remove date prefix
name = re.sub(r'^MOC[_-]', '', name) # Remove MOC prefix
# Split on common separators and filter
words = re.split(r'[_\-\s]+', name)
topics = []
# Filter out common words and keep meaningful terms
stop_words = {'and', 'the', 'for', 'with', 'to', 'of', 'in', 'on', 'at', 'by'}
for word in words:
if len(word) > 2 and word.lower() not in stop_words:
topics.append(word.lower())
return topics
def generate_moc_content(self, directory_path, title, description=""):
"""Generate MOC content for a directory."""
stats = self.analyze_directory(directory_path)
# Create frontmatter
frontmatter = f"""---
tags: [MOC, {directory_path.name.lower().replace(' ', '-')}]
type: map-of-content
created: {datetime.now().strftime('%Y-%m-%d')}
modified: {datetime.now().strftime('%Y-%m-%d')}
status: active
cssclass: moc
aliases: [{title} Hub, {title} Overview]
hub_for: [{directory_path.name}]
related_mocs: []
---
"""
# Create content
content = f"""# {title} Map of Content
## Overview
{description if description else f"This MOC organizes all content related to {title.lower()}."}
**Directory**: `{directory_path.name}/`
**Total Files**: {stats['md_files']} markdown files
**Last Updated**: {datetime.now().strftime('%Y-%m-%d')}
"""
# Add subdirectories if any
if stats['subdirectories']:
content += "## Subdirectories\n\n"
for subdir in sorted(stats['subdirectories']):
content += f"### {subdir}\n"
content += f"- [[MOC - {subdir}|{subdir} Overview]]\n\n"
# Organize files by topic
content += self.organize_files_by_topic(directory_path, stats)
# Add common topics section
if stats['common_topics']:
content += "## Key Topics\n\n"
sorted_topics = sorted(stats['common_topics'].items(), key=lambda x: x[1], reverse=True)
for topic, count in sorted_topics[:10]: # Top 10 topics
content += f"- **{topic.title()}** ({count} files)\n"
content += "\n"
# Add templates and resources
content += """## Related MOCs
- [[MOC - AI Development|AI Development]]
- [[MOC - Learning Resources|Learning Resources]]
- [[Master Index|🗂️ Master Index]]
## Status & Progress
- [ ] Organize files by topic
- [ ] Add cross-references
- [ ] Review and update links
- [ ] Add examples and tutorials
## Next Steps
- Review all files in this directory
- Create sub-MOCs for large topic clusters
- Add relevant tags to all files
- Connect to related MOCs
---
*This MOC was auto-generated. Please review and customize as needed.*
"""
return frontmatter + content
def organize_files_by_topic(self, directory_path, stats):
"""Organize files by topic clusters."""
content = "## Content Organization\n\n"
# Group files by topic
topic_files = defaultdict(list)
ungrouped_files = []
try:
for file_path in directory_path.glob('*.md'):
if file_path.name.startswith('MOC'):
continue # Skip existing MOCs
topics = self.extract_topics_from_filename(file_path.name)
if topics:
# Use first topic as primary grouping
primary_topic = topics[0]
topic_files[primary_topic].append(file_path)
else:
ungrouped_files.append(file_path)
except Exception as e:
print(f"Error organizing files: {e}")
return content
# Sort topics by number of files
sorted_topics = sorted(topic_files.items(), key=lambda x: len(x[1]), reverse=True)
for topic, files in sorted_topics:
if len(files) > 1: # Only create sections for topics with multiple files
content += f"### {topic.title()}\n"
for file_path in sorted(files):
title = file_path.stem.replace('_', ' ').replace('-', ' ')
content += f"- [[{title}]]\n"
content += "\n"
# Add ungrouped files
if ungrouped_files:
content += "### Other Files\n"
for file_path in sorted(ungrouped_files):
title = file_path.stem.replace('_', ' ').replace('-', ' ')
content += f"- [[{title}]]\n"
content += "\n"
return content
def create_moc_file(self, directory_path, title, description="", output_path=None):
"""Create a MOC file for a directory."""
content = self.generate_moc_content(directory_path, title, description)
if output_path is None:
# Create MOCs in the centralized map-of-content directory
moc_dir = self.vault_path / "map-of-content"
moc_dir.mkdir(exist_ok=True)
output_path = moc_dir / f"MOC - {title}.md"
# Check if file already exists
if output_path.exists():
print(f"MOC already exists: {output_path}")
return False
try:
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Created MOC: {output_path}")
return True
except Exception as e:
print(f"Error creating MOC: {e}")
return False
def suggest_mocs(self):
"""Suggest MOCs for directories that don't have them."""
suggestions = []
# Skip these directories
skip_dirs = {'.obsidian', '.trash', 'System_Files', '.git'}
for directory in self.vault_path.iterdir():
if not directory.is_dir() or directory.name in skip_dirs:
continue
stats = self.analyze_directory(directory)
# Check if directory has enough content to warrant a MOC
if stats['md_files'] >= 3:
# Check if MOC already exists
existing_mocs = list(directory.glob('MOC*.md'))
if not existing_mocs:
suggestions.append({
'directory': directory,
'title': directory.name,
'file_count': stats['md_files'],
'subdirs': len(stats['subdirectories']),
'top_topics': sorted(stats['common_topics'].items(),
key=lambda x: x[1], reverse=True)[:5]
})
return suggestions
def main():
parser = argparse.ArgumentParser(description='Generate MOCs for Obsidian vault')
parser.add_argument('--vault', default='/Users/cam/VAULT01',
help='Path to Obsidian vault')
parser.add_argument('--directory',
help='Specific directory to create MOC for')
parser.add_argument('--title',
help='Title for the MOC')
parser.add_argument('--description',
help='Description for the MOC')
parser.add_argument('--suggest', action='store_true',
help='Suggest directories that need MOCs')
parser.add_argument('--create-all', action='store_true',
help='Create MOCs for all suggested directories')
args = parser.parse_args()
generator = MOCGenerator(args.vault)
if args.suggest or args.create_all:
suggestions = generator.suggest_mocs()
if args.suggest:
print("MOC Suggestions:")
print("="*50)
for suggestion in suggestions:
print(f"Directory: {suggestion['directory'].name}")
print(f" Files: {suggestion['file_count']}")
print(f" Subdirs: {suggestion['subdirs']}")
if suggestion['top_topics']:
topics = [f"{topic} ({count})" for topic, count in suggestion['top_topics']]
print(f" Topics: {', '.join(topics)}")
print()
if args.create_all:
for suggestion in suggestions:
generator.create_moc_file(
suggestion['directory'],
suggestion['title']
)
elif args.directory:
directory_path = Path(args.vault) / args.directory
if not directory_path.exists():
print(f"Directory not found: {directory_path}")
return
title = args.title or directory_path.name
description = args.description or ""
generator.create_moc_file(directory_path, title, description)
else:
parser.print_help()
if __name__ == '__main__':
main()
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Parse keyword connections from Link Suggestions Report."""
import re
import sys
from pathlib import Path
def parse_keyword_connections(report_path):
"""Parse keyword connections from the report and find meaningful ones."""
with open(report_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find the keyword-based section
keyword_section_start = content.find("## Keyword-Based Link Suggestions")
if keyword_section_start == -1:
print("Could not find keyword-based section")
return []
# Find the next section (orphaned notes)
orphaned_section_start = content.find("## Orphaned Notes", keyword_section_start)
if orphaned_section_start == -1:
keyword_section = content[keyword_section_start:]
else:
keyword_section = content[keyword_section_start:orphaned_section_start]
# Pattern to match connections
pattern = r'- \[\[([^\]]+)\]\] ↔ \[\[([^\]]+)\]\]\s*\n\s*Common words: ([^\n]+)'
connections = []
for match in re.finditer(pattern, keyword_section):
file1 = match.group(1).strip()
file2 = match.group(2).strip()
keywords = match.group(3).strip()
# Skip self-connections
if file1 == file2:
continue
# Count keywords
keyword_list = [k.strip() for k in keywords.split(',')]
keyword_count = len(keyword_list)
# Only include connections with 5+ keywords
if keyword_count >= 5:
connections.append({
'file1': file1,
'file2': file2,
'keywords': keyword_list,
'count': keyword_count
})
# Sort by keyword count descending
connections.sort(key=lambda x: x['count'], reverse=True)
return connections
def main():
report_path = Path("/Users/cam/VAULT01/System_Files/Link_Suggestions_Report.md")
connections = parse_keyword_connections(report_path)
print(f"Found {len(connections)} meaningful keyword connections (5+ keywords)\n")
# Group by keyword themes
tech_keywords = {'llm', 'langchain', 'langgraph', 'rag', 'embedding', 'vector', 'agent', 'model', 'api', 'mcp'}
framework_keywords = {'langchain', 'langgraph', 'fastapi', 'docker', 'cloudflare', 'supabase'}
company_keywords = {'openai', 'anthropic', 'google', 'microsoft', 'meta'}
concept_keywords = {'automation', 'workflow', 'pipeline', 'integration', 'generation', 'retrieval'}
tech_connections = []
framework_connections = []
company_connections = []
concept_connections = []
for conn in connections:
keywords_lower = [k.lower() for k in conn['keywords']]
tech_score = len([k for k in keywords_lower if any(tech in k for tech in tech_keywords)])
framework_score = len([k for k in keywords_lower if any(fw in k for fw in framework_keywords)])
company_score = len([k for k in keywords_lower if any(comp in k for comp in company_keywords)])
concept_score = len([k for k in keywords_lower if any(conc in k for conc in concept_keywords)])
if tech_score >= 2:
tech_connections.append(conn)
if framework_score >= 2:
framework_connections.append(conn)
if company_score >= 1:
company_connections.append(conn)
if concept_score >= 2:
concept_connections.append(conn)
print("## High-Priority Technical Connections")
print(f"Found {len(tech_connections)} connections with technical keywords\n")
for i, conn in enumerate(tech_connections[:20]): # Top 20
print(f"{i+1}. [[{conn['file1']}]] ↔ [[{conn['file2']}]]")
print(f" Keywords ({conn['count']}): {', '.join(conn['keywords'][:10])}")
print()
print("\n## Framework-Related Connections")
print(f"Found {len(framework_connections)} connections with framework keywords\n")
for i, conn in enumerate(framework_connections[:15]): # Top 15
print(f"{i+1}. [[{conn['file1']}]] ↔ [[{conn['file2']}]]")
print(f" Keywords ({conn['count']}): {', '.join(conn['keywords'][:10])}")
print()
print("\n## Company/Provider Connections")
print(f"Found {len(company_connections)} connections with company keywords\n")
for i, conn in enumerate(company_connections[:10]): # Top 10
print(f"{i+1}. [[{conn['file1']}]] ↔ [[{conn['file2']}]]")
print(f" Keywords ({conn['count']}): {', '.join(conn['keywords'][:10])}")
print()
print("\n## Concept/Workflow Connections")
print(f"Found {len(concept_connections)} connections with concept keywords\n")
for i, conn in enumerate(concept_connections[:10]): # Top 10
print(f"{i+1}. [[{conn['file1']}]] ↔ [[{conn['file2']}]]")
print(f" Keywords ({conn['count']}): {', '.join(conn['keywords'][:10])}")
print()
if __name__ == "__main__":
main()
@@ -0,0 +1,387 @@
#!/usr/bin/env python3
"""
Tag Standardizer for Obsidian Vault
Normalizes and standardizes tags across all notes.
"""
import os
import re
import yaml
from pathlib import Path
from collections import defaultdict, Counter
import argparse
class TagStandardizer:
def __init__(self, vault_path):
self.vault_path = Path(vault_path)
self.tag_mappings = {}
self.tag_stats = Counter()
self.files_processed = 0
self.files_updated = 0
# Define standard tag mappings
self.standard_mappings = {
# Color codes to semantic tags
'#F0EEE6': 'clippings',
'#e0e0e0': 'reference',
'#f0f0f0': 'note',
# Technology standardization
'langchain': 'langchain',
'lang-chain': 'langchain',
'LangChain': 'langchain',
'langgraph': 'langgraph',
'lang-graph': 'langgraph',
'LangGraph': 'langgraph',
'mcp': 'mcp',
'MCP': 'mcp',
'model-context-protocol': 'mcp',
'Model Context Protocol': 'mcp',
'graphrag': 'graphrag',
'GraphRAG': 'graphrag',
'graph-rag': 'graphrag',
'openai': 'openai',
'OpenAI': 'openai',
'anthropic': 'anthropic',
'Anthropic': 'anthropic',
'claude': 'anthropic',
'Claude': 'anthropic',
'llm': 'ai/llm',
'LLM': 'ai/llm',
'ai-agents': 'ai/agents',
'AI Agents': 'ai/agents',
'embeddings': 'ai/embeddings',
'vector-db': 'ai/embeddings',
'rag': 'ai/embeddings',
'RAG': 'ai/embeddings',
# Common case standardizations
'MOC': 'moc',
'API': 'api',
'RSS': 'rss',
'UI': 'ui',
'AI': 'ai',
# Content type standardization
'research': 'research',
'Research': 'research',
'tutorial': 'tutorial',
'Tutorial': 'tutorial',
'how-to': 'tutorial',
'guide': 'tutorial',
'reference': 'reference',
'Reference': 'reference',
'docs': 'reference',
'documentation': 'reference',
'idea': 'idea',
'Idea': 'idea',
'ideas': 'idea',
'brainstorm': 'idea',
'concept': 'idea',
'meeting': 'meeting',
'Meeting': 'meeting',
'notes': 'meeting',
'email': 'email',
'Email': 'email',
'correspondence': 'email',
'daily': 'daily',
'Daily': 'daily',
'journal': 'daily',
'Journal': 'daily',
'log': 'daily',
# Business tags
'client': 'client',
'Client': 'client',
'business': 'business',
'Business': 'business',
'startup': 'startup',
'Startup': 'startup',
'freelance': 'freelance',
'Freelance': 'freelance',
'project': 'project',
'Project': 'project',
# Status tags
'active': 'status/active',
'Active': 'status/active',
'draft': 'status/draft',
'Draft': 'status/draft',
'completed': 'status/completed',
'Completed': 'status/completed',
'archived': 'status/archived',
'Archived': 'status/archived',
'todo': 'action/todo',
'TODO': 'action/todo',
'follow-up': 'action/follow-up',
'Follow-up': 'action/follow-up',
# Learning tags
'course': 'learning/course',
'Course': 'learning/course',
'certification': 'learning/certification',
'Certification': 'learning/certification',
'book': 'learning/book',
'Book': 'learning/book',
'video': 'learning/video',
'Video': 'learning/video',
'podcast': 'learning/podcast',
'Podcast': 'learning/podcast',
'conference': 'learning/conference',
'Conference': 'learning/conference',
'webinar': 'learning/webinar',
'Webinar': 'learning/webinar'
}
def extract_frontmatter(self, content):
"""Extract YAML frontmatter from content."""
if not content.strip().startswith('---'):
return None, content
# Find the end of frontmatter
lines = content.split('\n')
if len(lines) < 3:
return None, content
end_idx = None
for i in range(1, len(lines)):
if lines[i].strip() == '---':
end_idx = i
break
if end_idx is None:
return None, content
try:
frontmatter_text = '\n'.join(lines[1:end_idx])
frontmatter = yaml.safe_load(frontmatter_text)
remaining_content = '\n'.join(lines[end_idx + 1:])
return frontmatter, remaining_content
except yaml.YAMLError:
return None, content
def normalize_tag(self, tag):
"""Normalize a single tag."""
# Remove leading/trailing whitespace
tag = tag.strip()
# Remove hash if present
if tag.startswith('#'):
tag = tag[1:]
# Apply standard mappings
if tag in self.standard_mappings:
return self.standard_mappings[tag]
# Handle date-based tags
if re.match(r'\d{4}/\d{2}', tag):
return f'daily/{tag}'
# Handle file paths as tags
if '/' in tag and not tag.startswith(('ai/', 'client/', 'learning/', 'status/', 'action/')):
# Convert path-like tags to hierarchical tags
parts = tag.split('/')
if len(parts) >= 2:
category = parts[0].lower()
if category in ['ai', 'client', 'learning', 'business', 'project']:
return f"{category}/{'/'.join(parts[1:]).lower()}"
# Default normalization
return tag.lower().replace(' ', '-')
def standardize_tags(self, tags):
"""Standardize a list of tags."""
if not tags:
return []
# Handle different tag formats
if isinstance(tags, str):
# Single tag as string
return [self.normalize_tag(tags)]
if isinstance(tags, list):
standardized = []
for tag in tags:
if isinstance(tag, str):
normalized = self.normalize_tag(tag)
if normalized and normalized not in standardized:
standardized.append(normalized)
self.tag_stats[normalized] += 1
return standardized
return []
def process_file(self, file_path, dry_run=False):
"""Process a single file and standardize its tags."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
frontmatter, remaining_content = self.extract_frontmatter(content)
if frontmatter is None:
return False # No frontmatter to process
# Get current tags
current_tags = frontmatter.get('tags', [])
# Standardize tags
standardized_tags = self.standardize_tags(current_tags)
# Check if tags changed
if standardized_tags != current_tags:
if not dry_run:
# Update frontmatter
frontmatter['tags'] = standardized_tags
# Reconstruct content
new_frontmatter = yaml.dump(frontmatter, default_flow_style=False, sort_keys=False)
new_content = f"---\n{new_frontmatter}---\n{remaining_content}"
# Write back to file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
return True # File was updated
return False # No changes needed
except Exception as e:
print(f"Error processing {file_path}: {e}")
return False
def analyze_existing_tags(self):
"""Analyze existing tags in the vault."""
tag_files = defaultdict(list)
for file_path in self.vault_path.rglob('*.md'):
if any(skip_dir in file_path.parts for skip_dir in {'.obsidian', '.trash', 'System_Files', '.git'}):
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
frontmatter, _ = self.extract_frontmatter(content)
if frontmatter and 'tags' in frontmatter:
tags = frontmatter['tags']
if isinstance(tags, list):
for tag in tags:
if isinstance(tag, str):
tag_files[tag].append(file_path)
elif isinstance(tags, str):
tag_files[tags].append(file_path)
except Exception as e:
print(f"Error analyzing {file_path}: {e}")
return tag_files
def generate_tag_report(self):
"""Generate a report of current tags and suggested changes."""
print("Analyzing existing tags...")
tag_files = self.analyze_existing_tags()
report = []
report.append("# Tag Standardization Report")
report.append(f"Generated for vault: {self.vault_path}")
report.append(f"Total unique tags: {len(tag_files)}")
report.append("")
# Group tags by frequency
tag_frequency = [(tag, len(files)) for tag, files in tag_files.items()]
tag_frequency.sort(key=lambda x: x[1], reverse=True)
report.append("## Current Tags by Frequency")
for tag, count in tag_frequency:
normalized = self.normalize_tag(tag)
if normalized != tag:
report.append(f"- `{tag}` ({count} files) → `{normalized}`")
else:
report.append(f"- `{tag}` ({count} files)")
report.append("")
# Suggest consolidations
report.append("## Suggested Consolidations")
consolidations = defaultdict(list)
for tag, files in tag_files.items():
normalized = self.normalize_tag(tag)
if normalized != tag:
consolidations[normalized].append((tag, len(files)))
for normalized_tag, original_tags in consolidations.items():
if len(original_tags) > 1:
report.append(f"### {normalized_tag}")
total_files = sum(count for _, count in original_tags)
report.append(f"Total files: {total_files}")
for original, count in original_tags:
report.append(f"- `{original}` ({count} files)")
report.append("")
return '\n'.join(report)
def process_vault(self, dry_run=False):
"""Process all files in the vault."""
skip_dirs = {'.obsidian', '.trash', 'System_Files', '.git'}
for file_path in self.vault_path.rglob('*.md'):
if any(skip_dir in file_path.parts for skip_dir in skip_dirs):
continue
self.files_processed += 1
if self.process_file(file_path, dry_run):
self.files_updated += 1
if not dry_run:
print(f"Updated: {file_path.relative_to(self.vault_path)}")
return {
'processed': self.files_processed,
'updated': self.files_updated,
'tag_stats': dict(self.tag_stats)
}
def main():
parser = argparse.ArgumentParser(description='Standardize tags in Obsidian vault')
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 changed without making changes')
parser.add_argument('--report', action='store_true',
help='Generate analysis report of current tags')
parser.add_argument('--output',
default='/Users/cam/VAULT01/System_Files/Tag_Analysis_Report.md',
help='Output file for report')
args = parser.parse_args()
standardizer = TagStandardizer(args.vault)
if args.report:
report = standardizer.generate_tag_report()
with open(args.output, 'w', encoding='utf-8') as f:
f.write(report)
print(f"Tag analysis report saved to: {args.output}")
else:
print(f"Processing vault at: {args.vault}")
print("Dry run mode" if args.dry_run else "Making changes")
print("-" * 50)
results = standardizer.process_vault(dry_run=args.dry_run)
print("-" * 50)
print(f"Files processed: {results['processed']}")
print(f"Files updated: {results['updated']}")
if results['tag_stats']:
print("\nTop standardized tags:")
for tag, count in Counter(results['tag_stats']).most_common(10):
print(f" {tag}: {count}")
if __name__ == '__main__':
main()