Skill Numbering Changes: - 01-03: OurDigital core (was 30-32) - 31-32: Notion tools (was 01-02) - 99_archive: Renamed from _archive for sorting New Files: - AGENTS.md: Claude Code agent routing guide - requirements.txt for 00-claude-code-setting, 32-notion-writer, 43-jamie-youtube-manager Documentation Updates: - CLAUDE.md: Updated skill inventory (23 skills) - AUDIT_REPORT.md: Current completion status (91%) - Archived REFACTORING_PLAN.md (most tasks complete) Removed: - ga-agent-skills/ (moved to separate repo ~/Project/dintel-ga4-agent) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Ulysses Export Helper
|
|
|
|
Exports markdown files to iCloud folder for Ulysses sync.
|
|
Designed for OurDigital research-to-publisher workflow.
|
|
|
|
Usage:
|
|
python export_to_ulysses.py --content "# Title\n\nContent..." --filename "my-post.md"
|
|
python export_to_ulysses.py --file /path/to/draft.md --channel blog
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import argparse
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
# Default iCloud paths (user should customize)
|
|
ICLOUD_BASE = Path.home() / "Library/Mobile Documents/com~apple~CloudDocs"
|
|
|
|
# Channel-specific folders (customize based on Ulysses library structure)
|
|
CHANNEL_FOLDERS = {
|
|
"blog": "Ulysses/OurDigital/Blog Drafts",
|
|
"journal": "Ulysses/OurDigital/Journal Drafts",
|
|
"ourstory": "Ulysses/OurDigital/OurStory Drafts",
|
|
"medium": "Ulysses/OurDigital/Medium Drafts",
|
|
"default": "Ulysses/Blog Drafts"
|
|
}
|
|
|
|
def get_export_path(channel: str = "default") -> Path:
|
|
"""Get the export path for a specific channel."""
|
|
folder = CHANNEL_FOLDERS.get(channel, CHANNEL_FOLDERS["default"])
|
|
export_path = ICLOUD_BASE / folder
|
|
|
|
# Create directory if it doesn't exist
|
|
export_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
return export_path
|
|
|
|
def generate_filename(title: str = None) -> str:
|
|
"""Generate a filename with timestamp."""
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
|
if title:
|
|
# Clean title for filename
|
|
clean_title = "".join(c if c.isalnum() or c in "- " else "" for c in title)
|
|
clean_title = clean_title.replace(" ", "-").lower()[:50]
|
|
return f"{timestamp}_{clean_title}.md"
|
|
return f"{timestamp}_draft.md"
|
|
|
|
def export_content(content: str, filename: str, channel: str = "default") -> Path:
|
|
"""Export content to the appropriate iCloud folder."""
|
|
export_path = get_export_path(channel)
|
|
file_path = export_path / filename
|
|
|
|
# Add Ulysses-friendly frontmatter if not present
|
|
if not content.startswith("---"):
|
|
frontmatter = f"""---
|
|
created: {datetime.now().isoformat()}
|
|
channel: {channel}
|
|
status: draft
|
|
---
|
|
|
|
"""
|
|
content = frontmatter + content
|
|
|
|
file_path.write_text(content, encoding="utf-8")
|
|
return file_path
|
|
|
|
def export_file(source_path: str, channel: str = "default") -> Path:
|
|
"""Copy an existing file to the iCloud folder."""
|
|
source = Path(source_path)
|
|
if not source.exists():
|
|
raise FileNotFoundError(f"Source file not found: {source_path}")
|
|
|
|
content = source.read_text(encoding="utf-8")
|
|
filename = source.name
|
|
|
|
return export_content(content, filename, channel)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Export markdown to Ulysses via iCloud")
|
|
parser.add_argument("--content", help="Markdown content to export")
|
|
parser.add_argument("--file", help="Path to existing markdown file")
|
|
parser.add_argument("--filename", help="Output filename (auto-generated if not provided)")
|
|
parser.add_argument("--channel", choices=list(CHANNEL_FOLDERS.keys()),
|
|
default="default", help="Target channel/folder")
|
|
parser.add_argument("--list-paths", action="store_true",
|
|
help="List configured export paths")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.list_paths:
|
|
print("Configured export paths:")
|
|
for channel, folder in CHANNEL_FOLDERS.items():
|
|
full_path = ICLOUD_BASE / folder
|
|
exists = "✓" if full_path.exists() else "✗"
|
|
print(f" [{exists}] {channel}: {full_path}")
|
|
return
|
|
|
|
if args.file:
|
|
result = export_file(args.file, args.channel)
|
|
print(f"✓ Exported to: {result}")
|
|
elif args.content:
|
|
filename = args.filename or generate_filename()
|
|
result = export_content(args.content, filename, args.channel)
|
|
print(f"✓ Exported to: {result}")
|
|
else:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|