Initial commit
This commit is contained in:
303
skills/obsidian-manager/SKILL.md
Normal file
303
skills/obsidian-manager/SKILL.md
Normal file
@@ -0,0 +1,303 @@
|
||||
---
|
||||
name: obsidian-manager
|
||||
description: Manage Obsidian vault for persistent knowledge storage, search, and retrieval
|
||||
triggers:
|
||||
- "save to obsidian"
|
||||
- "search my notes"
|
||||
- "find in vault"
|
||||
- "check my readwise"
|
||||
- "search readwise"
|
||||
- "search snipd"
|
||||
- "check my highlights"
|
||||
- "create note"
|
||||
- "add to obsidian"
|
||||
- "what did I read about"
|
||||
- "what podcasts mentioned"
|
||||
allowed-tools: Read, Write, Bash, Glob, Grep
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Obsidian Manager Skill
|
||||
|
||||
Manage the user's Obsidian vault as a persistent second brain. Read existing knowledge, write new content, and search across years of highlights and notes.
|
||||
|
||||
## Vault Configuration
|
||||
|
||||
**Path:** `/Users/hagelk/Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes/`
|
||||
|
||||
**Write Mode:** Auto-create new files, confirm before updating existing files.
|
||||
|
||||
## Vault Structure
|
||||
|
||||
```
|
||||
Personal_Notes/
|
||||
├── Geoffrey/ # Geoffrey-generated content
|
||||
│ ├── Research/ # Research task outputs
|
||||
│ ├── Reports/ # Generated reports and artifacts
|
||||
│ │ ├── PSD/ # Peninsula School District reports
|
||||
│ │ │ ├── Discipline/ # Discipline reports and infographics
|
||||
│ │ │ ├── Enrollment/ # Enrollment reports
|
||||
│ │ │ └── {topic}/ # Other PSD topics
|
||||
│ │ ├── HRG/ # Hat Rack Group (consulting LLC) reports
|
||||
│ │ │ └── {topic}/ # HRG topics
|
||||
│ │ ├── Personal/ # Personal reports
|
||||
│ │ │ └── {topic}/ # Personal topics
|
||||
│ │ └── {org}/ # Other organizations as needed
|
||||
│ ├── Daily-Logs/ # Session summaries
|
||||
│ ├── Learnings/ # Extracted patterns
|
||||
│ └── Decisions/ # Major decisions with rationale
|
||||
├── Meetings/ # Meeting notes (YYYY-MM-DD format)
|
||||
├── People/ # Contact profiles with metadata
|
||||
├── Readwise/ # Synced highlights (368 articles, 34 books)
|
||||
│ ├── Articles/
|
||||
│ ├── Books/
|
||||
│ ├── Podcasts/
|
||||
│ └── Tweets/
|
||||
├── Snipd/ # Podcast highlights (58+ episodes)
|
||||
│ └── Data/ # Podcast folders > episodes
|
||||
└── Templates/ # Note templates
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
All scripts use Python via `uv run` with inline dependencies.
|
||||
|
||||
### Search Scripts
|
||||
|
||||
#### search.py - General Vault Search
|
||||
```bash
|
||||
uv run scripts/search.py "query" [folder] [--limit N]
|
||||
```
|
||||
|
||||
Search across the vault for keyword matches. Returns file paths and matching excerpts.
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Search entire vault
|
||||
uv run scripts/search.py "prompt engineering"
|
||||
|
||||
# Search specific folder
|
||||
uv run scripts/search.py "leadership" People
|
||||
|
||||
# Limit results
|
||||
uv run scripts/search.py "AI" Readwise/Articles --limit 10
|
||||
```
|
||||
|
||||
#### search-readwise.py - Search Highlights
|
||||
```bash
|
||||
uv run scripts/search-readwise.py "query" [--author "name"] [--category articles|books|podcasts]
|
||||
```
|
||||
|
||||
Search Readwise highlights by topic, author, or category.
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Search by topic
|
||||
uv run scripts/search-readwise.py "second brain"
|
||||
|
||||
# Search specific author
|
||||
uv run scripts/search-readwise.py "AI" --author "Andrej Karpathy"
|
||||
|
||||
# Search books only
|
||||
uv run scripts/search-readwise.py "habits" --category books
|
||||
```
|
||||
|
||||
#### search-snipd.py - Search Podcast Transcripts
|
||||
```bash
|
||||
uv run scripts/search-snipd.py "query" [--show "podcast name"]
|
||||
```
|
||||
|
||||
Search Snipd podcast transcripts and snips.
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Search all podcasts
|
||||
uv run scripts/search-snipd.py "machine learning"
|
||||
|
||||
# Search specific show
|
||||
uv run scripts/search-snipd.py "leadership" --show "Huberman Lab"
|
||||
```
|
||||
|
||||
### Creation Scripts
|
||||
|
||||
#### create-note.py - Create New Note
|
||||
```bash
|
||||
uv run scripts/create-note.py "title" "content" [--folder Geoffrey/Research] [--tags tag1,tag2] [--related "[[Note]]"]
|
||||
```
|
||||
|
||||
Create a new note with proper frontmatter and optional backlinks.
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Create research note
|
||||
uv run scripts/create-note.py "AI Model Comparison" "Content here..." --folder Geoffrey/Research --tags research,ai
|
||||
|
||||
# Create with backlinks
|
||||
uv run scripts/create-note.py "Meeting Notes" "Content..." --folder Meetings --related "[[John Smith]],[[Project Alpha]]"
|
||||
```
|
||||
|
||||
#### open-in-obsidian.py - Open Note in Obsidian
|
||||
```bash
|
||||
uv run scripts/open-in-obsidian.py "path/to/note.md"
|
||||
```
|
||||
|
||||
Opens the specified note in Obsidian using Actions URI.
|
||||
|
||||
## Content Routing
|
||||
|
||||
**CRITICAL:** When creating content, use these destinations. NEVER dump files directly into `Geoffrey/` - always use proper subfolders.
|
||||
|
||||
| Content Type | Folder | Naming Pattern |
|
||||
|--------------|--------|----------------|
|
||||
| Research results | `Geoffrey/Research/` | `YYYY-MM-DD-topic.md` |
|
||||
| **Reports** | `Geoffrey/Reports/{org}/{topic}/` | Descriptive name with date |
|
||||
| **Images/Infographics** | Same as parent report | `Descriptive Name YYYY-MM-DD.png` |
|
||||
| Meeting notes | `Meetings/` | `Topic - YYYY-MM-DD.md` |
|
||||
| Daily summaries | `Geoffrey/Daily-Logs/` | `YYYY-MM-DD.md` |
|
||||
| Learnings | `Geoffrey/Learnings/` | `topic-slug.md` |
|
||||
| Decisions | `Geoffrey/Decisions/` | `YYYY-MM-DD-decision.md` |
|
||||
| Person updates | `People/` | Existing file name |
|
||||
|
||||
**Naming Rules:**
|
||||
- All images/infographics MUST include date in filename
|
||||
- Format: `Descriptive Name YYYY-MM-DD.png` or `Descriptive Name YYYY-MM-DD to YYYY-MM-DD.png` for date ranges
|
||||
- Example: `Peninsula High School Discipline Report 2025-11-17 to 2025-11-20.png`
|
||||
|
||||
**Reports Folder Structure:**
|
||||
Pattern: `Geoffrey/Reports/{organization}/{topic}/`
|
||||
|
||||
Organizations:
|
||||
- **PSD** - Peninsula School District reports
|
||||
- `Geoffrey/Reports/PSD/Discipline/`
|
||||
- `Geoffrey/Reports/PSD/Enrollment/`
|
||||
- `Geoffrey/Reports/PSD/Attendance/`
|
||||
- `Geoffrey/Reports/PSD/{topic}/`
|
||||
|
||||
- **HRG** - Hat Rack Group (consulting LLC) reports
|
||||
- `Geoffrey/Reports/HRG/{topic}/`
|
||||
|
||||
- **Personal** - Personal reports
|
||||
- `Geoffrey/Reports/Personal/{topic}/`
|
||||
|
||||
- **{org}** - Other organizations as needed
|
||||
|
||||
## Frontmatter Standard
|
||||
|
||||
All Geoffrey-created notes should include:
|
||||
|
||||
```yaml
|
||||
---
|
||||
created: 2025-11-23
|
||||
tags: [geoffrey, research]
|
||||
source: geoffrey
|
||||
related:
|
||||
- "[[Related Note 1]]"
|
||||
- "[[Related Note 2]]"
|
||||
---
|
||||
```
|
||||
|
||||
## When to Read from Vault
|
||||
|
||||
- **Before research tasks:** Check if user has relevant Readwise highlights or existing notes
|
||||
- **When people mentioned:** Look up context in People folder
|
||||
- **For meeting prep:** Find past meetings and related notes
|
||||
- **For context:** Search Snipd transcripts for podcast discussions on topic
|
||||
|
||||
## When to Write to Vault
|
||||
|
||||
- **After research:** Save results to `Geoffrey/Research/`
|
||||
- **Learned patterns:** Extract insights to `Geoffrey/Learnings/`
|
||||
- **Major decisions:** Document with rationale in `Geoffrey/Decisions/`
|
||||
- **Session summaries:** Optional daily logs to `Geoffrey/Daily-Logs/`
|
||||
|
||||
## Creating Backlinks
|
||||
|
||||
Always create backlinks to existing content:
|
||||
|
||||
1. **Search before creating:** Find related notes first
|
||||
2. **Use wiki-links:** `[[Note Title]]` format
|
||||
3. **Add to frontmatter:** List in `related:` field
|
||||
4. **Reference sources:** Link to Readwise/Snipd sources when relevant
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
Based on insights from [[Readwise/Articles/Building a Second Brain]],
|
||||
and discussed in [[Snipd/Data/Huberman Lab/episode-123]],
|
||||
the key pattern is...
|
||||
```
|
||||
|
||||
## Readwise Content Format
|
||||
|
||||
Readwise notes have this structure:
|
||||
|
||||
```yaml
|
||||
---
|
||||
Author: "Author Name"
|
||||
Full Title: "Article Title"
|
||||
Category: #articles
|
||||
Summary: "AI-generated summary"
|
||||
---
|
||||
|
||||
## Highlights
|
||||
- Highlight text with optional tags like [[topic]]
|
||||
```
|
||||
|
||||
## Snipd Content Format
|
||||
|
||||
Snipd episodes have rich metadata:
|
||||
|
||||
```yaml
|
||||
---
|
||||
episode_show: "Podcast Name"
|
||||
episode_publish_date: 2024-01-15
|
||||
mentioned_books: ["Book 1", "Book 2"]
|
||||
snips_count: 5
|
||||
---
|
||||
|
||||
### Snip 1
|
||||
**Timestamp:** 00:15:30
|
||||
> Quote text here
|
||||
|
||||
**Transcript:** Full context...
|
||||
```
|
||||
|
||||
## Actions URI Integration
|
||||
|
||||
Use Actions URI to interact with Obsidian app:
|
||||
|
||||
```bash
|
||||
# Open a note
|
||||
open "obsidian://actions-uri/note/open?vault=Personal_Notes&file=Geoffrey/Research/note.md"
|
||||
|
||||
# Create and open
|
||||
open "obsidian://actions-uri/note/create?vault=Personal_Notes&file=path&content=..."
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Search before creating:** Always check for existing content
|
||||
2. **Link generously:** Create connections between notes
|
||||
3. **Use consistent tags:** Match existing tag patterns
|
||||
4. **Keep notes atomic:** One idea per note in Learnings
|
||||
5. **Include sources:** Always cite Readwise/Snipd sources
|
||||
6. **Respect existing structure:** Don't reorganize user's folders
|
||||
|
||||
## Output Format
|
||||
|
||||
When completing Obsidian operations:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
What was done
|
||||
|
||||
## Files
|
||||
- Created: `path/to/new.md`
|
||||
- Updated: `path/to/existing.md` (if confirmed)
|
||||
|
||||
## Related Content Found
|
||||
- [[Relevant Note 1]] - why it's relevant
|
||||
- [[Relevant Note 2]] - why it's relevant
|
||||
|
||||
## Next Steps
|
||||
- Recommendations if any
|
||||
```
|
||||
147
skills/obsidian-manager/scripts/create-note.py
Normal file
147
skills/obsidian-manager/scripts/create-note.py
Normal 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()
|
||||
77
skills/obsidian-manager/scripts/open-in-obsidian.py
Normal file
77
skills/obsidian-manager/scripts/open-in-obsidian.py
Normal 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()
|
||||
169
skills/obsidian-manager/scripts/search-readwise.py
Normal file
169
skills/obsidian-manager/scripts/search-readwise.py
Normal 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()
|
||||
136
skills/obsidian-manager/scripts/search-snipd.py
Normal file
136
skills/obsidian-manager/scripts/search-snipd.py
Normal 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()
|
||||
116
skills/obsidian-manager/scripts/search.py
Normal file
116
skills/obsidian-manager/scripts/search.py
Normal 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()
|
||||
Reference in New Issue
Block a user