Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:35:59 +08:00
commit 90883a4d25
287 changed files with 75058 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["python-frontmatter", "pyyaml"]
# ///
"""
Create a new note in the Obsidian vault with proper frontmatter.
Usage:
uv run create-note.py "title" "content" [--folder path] [--tags tag1,tag2] [--related "[[Note]]"]
Arguments:
title - Note title (will be used for filename)
content - Note content (markdown)
--folder - Folder path relative to vault (default: Geoffrey/Research)
--tags - Comma-separated tags
--related - Comma-separated related notes as wiki-links
Examples:
uv run create-note.py "AI Model Comparison" "Content..." --folder Geoffrey/Research --tags research,ai
uv run create-note.py "Meeting Notes" "Content..." --folder Meetings --related "[[John Smith]],[[Project]]"
"""
import sys
import os
import json
import re
from pathlib import Path
from datetime import datetime
import frontmatter
VAULT_PATH = Path.home() / "Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes"
def slugify(title: str) -> str:
"""Convert title to filename-safe slug."""
# Replace spaces with hyphens, remove special chars
slug = re.sub(r'[^\w\s-]', '', title.lower())
slug = re.sub(r'[-\s]+', '-', slug).strip('-')
return slug
def create_note(
title: str,
content: str,
folder: str = "Geoffrey/Research",
tags: list[str] = None,
related: list[str] = None
) -> str:
"""Create a new note with frontmatter."""
# Ensure folder exists
folder_path = VAULT_PATH / folder
folder_path.mkdir(parents=True, exist_ok=True)
# Generate filename
date_prefix = datetime.now().strftime("%Y-%m-%d")
slug = slugify(title)
filename = f"{date_prefix}-{slug}.md"
file_path = folder_path / filename
# Check if file exists
if file_path.exists():
# Add timestamp to make unique
timestamp = datetime.now().strftime("%H%M%S")
filename = f"{date_prefix}-{slug}-{timestamp}.md"
file_path = folder_path / filename
# Build frontmatter
metadata = {
"created": datetime.now().strftime("%Y-%m-%d"),
"source": "geoffrey"
}
if tags:
metadata["tags"] = ["geoffrey"] + [t.strip() for t in tags if t.strip()]
else:
metadata["tags"] = ["geoffrey"]
if related:
metadata["related"] = [r.strip() for r in related if r.strip()]
# Create the post
post = frontmatter.Post(content)
post.metadata = metadata
# Add title as H1 if not already present
if not content.strip().startswith('#'):
post.content = f"# {title}\n\n{content}"
# Write file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(frontmatter.dumps(post))
return str(file_path.relative_to(VAULT_PATH))
def main():
if len(sys.argv) < 3:
print("Usage: uv run create-note.py \"title\" \"content\" [--folder path] [--tags t1,t2] [--related \"[[Note]]\"]")
sys.exit(1)
title = sys.argv[1]
content = sys.argv[2]
folder = "Geoffrey/Research"
tags = []
related = []
# Parse arguments
i = 3
while i < len(sys.argv):
if sys.argv[i] == "--folder" and i + 1 < len(sys.argv):
folder = sys.argv[i + 1]
i += 2
elif sys.argv[i] == "--tags" and i + 1 < len(sys.argv):
tags = sys.argv[i + 1].split(',')
i += 2
elif sys.argv[i] == "--related" and i + 1 < len(sys.argv):
related = sys.argv[i + 1].split(',')
i += 2
else:
i += 1
print(f"Creating note: {title}")
print(f"Folder: {folder}")
if tags:
print(f"Tags: {', '.join(tags)}")
if related:
print(f"Related: {', '.join(related)}")
relative_path = create_note(title, content, folder, tags, related)
full_path = VAULT_PATH / relative_path
print(f"\nNote created: {relative_path}")
# Output JSON for programmatic use
result = {
"success": True,
"path": relative_path,
"full_path": str(full_path),
"folder": folder,
"title": title
}
print(f"\n{json.dumps(result)}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# /// script
# dependencies = []
# ///
"""
Open a note in Obsidian using Actions URI.
Usage:
uv run open-in-obsidian.py "path/to/note.md"
Arguments:
path - Path to note relative to vault root
Examples:
uv run open-in-obsidian.py "Geoffrey/Research/2025-11-23-ai-comparison.md"
uv run open-in-obsidian.py "People/John Smith.md"
"""
import sys
import subprocess
import urllib.parse
from pathlib import Path
VAULT_NAME = "Personal_Notes"
VAULT_PATH = Path.home() / "Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes"
def open_in_obsidian(note_path: str) -> bool:
"""Open a note in Obsidian using Actions URI."""
# Remove .md extension if present (Actions URI doesn't need it)
if note_path.endswith('.md'):
note_path = note_path[:-3]
# URL encode the path
encoded_path = urllib.parse.quote(note_path, safe='')
# Build Actions URI
# Using actions-uri plugin: obsidian://actions-uri/note/open
uri = f"obsidian://actions-uri/note/open?vault={urllib.parse.quote(VAULT_NAME)}&file={encoded_path}"
try:
# Open the URI (macOS)
subprocess.run(['open', uri], check=True)
return True
except subprocess.CalledProcessError as e:
print(f"Error opening note: {e}")
return False
def main():
if len(sys.argv) < 2:
print("Usage: uv run open-in-obsidian.py \"path/to/note.md\"")
print("\nExamples:")
print(" uv run open-in-obsidian.py \"Geoffrey/Research/note.md\"")
print(" uv run open-in-obsidian.py \"People/John Smith.md\"")
sys.exit(1)
note_path = sys.argv[1]
# Verify file exists
full_path = VAULT_PATH / note_path
if not full_path.exists():
print(f"Warning: Note does not exist at {full_path}")
print("Attempting to open anyway (Obsidian may create it)...")
print(f"Opening in Obsidian: {note_path}")
if open_in_obsidian(note_path):
print("Note opened successfully")
else:
print("Failed to open note")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["python-frontmatter", "pyyaml"]
# ///
"""
Search Readwise highlights by topic, author, or category.
Usage:
uv run search-readwise.py "query" [--author "name"] [--category articles|books|podcasts|tweets]
Arguments:
query - Search term or phrase
--author - Filter by author name
--category - Filter by category (articles, books, podcasts, tweets)
Examples:
uv run search-readwise.py "second brain"
uv run search-readwise.py "AI" --author "Andrej Karpathy"
uv run search-readwise.py "habits" --category books
"""
import sys
import os
import json
import re
from pathlib import Path
import frontmatter
VAULT_PATH = Path.home() / "Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes"
READWISE_PATH = VAULT_PATH / "Readwise"
def search_readwise(query: str, author: str = "", category: str = "", limit: int = 15) -> list[dict]:
"""Search Readwise highlights."""
results = []
# Determine search paths based on category
if category:
category_map = {
"articles": "Articles",
"books": "Books",
"podcasts": "Podcasts",
"tweets": "Tweets"
}
search_paths = [READWISE_PATH / category_map.get(category.lower(), category)]
else:
search_paths = [
READWISE_PATH / "Articles",
READWISE_PATH / "Books",
READWISE_PATH / "Podcasts",
READWISE_PATH / "Tweets"
]
pattern = re.compile(re.escape(query), re.IGNORECASE)
author_pattern = re.compile(re.escape(author), re.IGNORECASE) if author else None
for search_path in search_paths:
if not search_path.exists():
continue
for md_file in search_path.rglob("*.md"):
try:
post = frontmatter.load(md_file)
content = post.content
metadata = post.metadata
# Filter by author if specified
if author_pattern:
file_author = metadata.get("Author", "")
if not author_pattern.search(file_author):
continue
# Search in content
matches = list(pattern.finditer(content))
if not matches:
continue
# Extract highlights (lines starting with - or >)
highlights = []
for line in content.split('\n'):
line = line.strip()
if (line.startswith('-') or line.startswith('>')) and pattern.search(line):
highlights.append(line[:200])
# Get context around first match if no highlight found
if not highlights and matches:
first_match = matches[0]
start = max(0, first_match.start() - 50)
end = min(len(content), first_match.end() + 150)
excerpt = content[start:end].strip()
excerpt = re.sub(r'\s+', ' ', excerpt)
highlights = [excerpt]
results.append({
"file": str(md_file.relative_to(VAULT_PATH)),
"title": metadata.get("Full Title", md_file.stem),
"author": metadata.get("Author", "Unknown"),
"category": search_path.name,
"matches": len(matches),
"highlights": highlights[:3] # Top 3 matching highlights
})
if len(results) >= limit:
break
except Exception as e:
continue
if len(results) >= limit:
break
# Sort by number of matches
results.sort(key=lambda x: x["matches"], reverse=True)
return results[:limit]
def main():
if len(sys.argv) < 2:
print("Usage: uv run search-readwise.py \"query\" [--author \"name\"] [--category type]")
sys.exit(1)
query = sys.argv[1]
author = ""
category = ""
# Parse arguments
i = 2
while i < len(sys.argv):
if sys.argv[i] == "--author" and i + 1 < len(sys.argv):
author = sys.argv[i + 1]
i += 2
elif sys.argv[i] == "--category" and i + 1 < len(sys.argv):
category = sys.argv[i + 1]
i += 2
else:
i += 1
filters = []
if author:
filters.append(f"author: {author}")
if category:
filters.append(f"category: {category}")
filter_str = f" ({', '.join(filters)})" if filters else ""
print(f"Searching Readwise for '{query}'{filter_str}...")
results = search_readwise(query, author, category)
if not results:
print(f"\nNo Readwise highlights found for '{query}'")
return
print(f"\nFound {len(results)} sources:\n")
for i, result in enumerate(results, 1):
print(f"{i}. **{result['title']}**")
print(f" Author: {result['author']} | Category: {result['category']} | {result['matches']} matches")
print(f" File: {result['file']}")
if result['highlights']:
print(f" Highlights:")
for h in result['highlights']:
print(f" - {h[:150]}{'...' if len(h) > 150 else ''}")
print()
# Output JSON for programmatic use
print(f"\n{json.dumps({'results': results, 'total': len(results)})}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["python-frontmatter", "pyyaml"]
# ///
"""
Search Snipd podcast transcripts and snips.
Usage:
uv run search-snipd.py "query" [--show "podcast name"]
Arguments:
query - Search term or phrase
--show - Filter by podcast show name
Examples:
uv run search-snipd.py "machine learning"
uv run search-snipd.py "leadership" --show "Huberman Lab"
"""
import sys
import os
import json
import re
from pathlib import Path
import frontmatter
VAULT_PATH = Path.home() / "Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes"
SNIPD_PATH = VAULT_PATH / "Snipd"
def search_snipd(query: str, show: str = "", limit: int = 15) -> list[dict]:
"""Search Snipd podcast transcripts."""
results = []
search_path = SNIPD_PATH / "Data"
if not search_path.exists():
search_path = SNIPD_PATH
pattern = re.compile(re.escape(query), re.IGNORECASE)
show_pattern = re.compile(re.escape(show), re.IGNORECASE) if show else None
for md_file in search_path.rglob("*.md"):
try:
post = frontmatter.load(md_file)
content = post.content
metadata = post.metadata
# Filter by show if specified
if show_pattern:
episode_show = metadata.get("episode_show", "")
if not show_pattern.search(episode_show):
continue
# Search in content
matches = list(pattern.finditer(content))
if not matches:
continue
# Extract snips (look for timestamp patterns and quotes)
snips = []
lines = content.split('\n')
for j, line in enumerate(lines):
if pattern.search(line):
# Get context (this line and surrounding)
start = max(0, j - 1)
end = min(len(lines), j + 2)
context = ' '.join(lines[start:end]).strip()
context = re.sub(r'\s+', ' ', context)
if context:
snips.append(context[:300])
results.append({
"file": str(md_file.relative_to(VAULT_PATH)),
"title": metadata.get("episode_title", md_file.stem),
"show": metadata.get("episode_show", "Unknown"),
"date": str(metadata.get("episode_publish_date", "")),
"snips_count": metadata.get("snips_count", 0),
"matches": len(matches),
"excerpts": snips[:3] # Top 3 matching excerpts
})
if len(results) >= limit:
break
except Exception as e:
continue
# Sort by number of matches
results.sort(key=lambda x: x["matches"], reverse=True)
return results[:limit]
def main():
if len(sys.argv) < 2:
print("Usage: uv run search-snipd.py \"query\" [--show \"podcast name\"]")
sys.exit(1)
query = sys.argv[1]
show = ""
# Parse arguments
i = 2
while i < len(sys.argv):
if sys.argv[i] == "--show" and i + 1 < len(sys.argv):
show = sys.argv[i + 1]
i += 2
else:
i += 1
filter_str = f" (show: {show})" if show else ""
print(f"Searching Snipd podcasts for '{query}'{filter_str}...")
results = search_snipd(query, show)
if not results:
print(f"\nNo Snipd episodes found for '{query}'")
return
print(f"\nFound {len(results)} episodes:\n")
for i, result in enumerate(results, 1):
print(f"{i}. **{result['title']}**")
print(f" Show: {result['show']} | Date: {result['date']} | Snips: {result['snips_count']} | {result['matches']} matches")
print(f" File: {result['file']}")
if result['excerpts']:
print(f" Excerpts:")
for e in result['excerpts']:
print(f" - {e[:200]}{'...' if len(e) > 200 else ''}")
print()
# Output JSON for programmatic use
print(f"\n{json.dumps({'results': results, 'total': len(results)})}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["python-frontmatter"]
# ///
"""
Search Obsidian vault for keyword matches.
Usage:
uv run search.py "query" [folder] [--limit N]
Arguments:
query - Search term or phrase
folder - Optional subfolder to search (default: entire vault)
--limit - Maximum results to return (default: 20)
Examples:
uv run search.py "prompt engineering"
uv run search.py "leadership" People
uv run search.py "AI" Readwise/Articles --limit 10
"""
import sys
import os
import json
import re
from pathlib import Path
VAULT_PATH = Path.home() / "Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes"
def search_vault(query: str, folder: str = "", limit: int = 20) -> list[dict]:
"""Search vault for query matches."""
results = []
search_path = VAULT_PATH / folder if folder else VAULT_PATH
if not search_path.exists():
print(f"Error: Path does not exist: {search_path}")
sys.exit(1)
# Case-insensitive search pattern
pattern = re.compile(re.escape(query), re.IGNORECASE)
for md_file in search_path.rglob("*.md"):
try:
content = md_file.read_text(encoding='utf-8')
matches = list(pattern.finditer(content))
if matches:
# Get context around first match
first_match = matches[0]
start = max(0, first_match.start() - 100)
end = min(len(content), first_match.end() + 100)
excerpt = content[start:end].strip()
# Clean up excerpt
excerpt = re.sub(r'\s+', ' ', excerpt)
if start > 0:
excerpt = "..." + excerpt
if end < len(content):
excerpt = excerpt + "..."
results.append({
"file": str(md_file.relative_to(VAULT_PATH)),
"matches": len(matches),
"excerpt": excerpt
})
if len(results) >= limit:
break
except Exception as e:
continue # Skip files that can't be read
# Sort by number of matches
results.sort(key=lambda x: x["matches"], reverse=True)
return results[:limit]
def main():
if len(sys.argv) < 2:
print("Usage: uv run search.py \"query\" [folder] [--limit N]")
sys.exit(1)
query = sys.argv[1]
folder = ""
limit = 20
# Parse arguments
i = 2
while i < len(sys.argv):
if sys.argv[i] == "--limit" and i + 1 < len(sys.argv):
limit = int(sys.argv[i + 1])
i += 2
else:
folder = sys.argv[i]
i += 1
print(f"Searching for '{query}' in {folder or 'entire vault'}...")
results = search_vault(query, folder, limit)
if not results:
print(f"\nNo results found for '{query}'")
return
print(f"\nFound {len(results)} results:\n")
for i, result in enumerate(results, 1):
print(f"{i}. **{result['file']}** ({result['matches']} matches)")
print(f" {result['excerpt']}\n")
# Output JSON for programmatic use
print(f"\n{json.dumps({'results': results, 'total': len(results)})}")
if __name__ == "__main__":
main()