Custom Skills (ourdigital-custom-skills/): - 00-ourdigital-visual-storytelling: Blog featured image prompt generator - 01-ourdigital-research-publisher: Research-to-publication workflow - 02-notion-organizer: Notion workspace management - 03-research-to-presentation: Notion research to PPT/Figma - 04-seo-gateway-strategist: SEO gateway page strategy planning - 05-gateway-page-content-builder: Gateway page content generation - 20-jamie-brand-editor: Jamie Clinic branded content GENERATION - 21-jamie-brand-guardian: Jamie Clinic content REVIEW & evaluation Refinements applied: - All skills converted to SKILL.md format with YAML frontmatter - Added version fields to all skills - Flattened nested folder structures - Removed packaging artifacts (.zip, .skill files) - Reorganized file structures (scripts/, references/, etc.) - Differentiated Jamie skills with clear roles 🤖 Generated with [Claude Code](https://claude.ai/code) 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()
|