feat: Add OurDigital custom skills package (10 skills)

Complete implementation of OurDigital skills with dual-platform support
(Claude Desktop + Claude Code) following standardized structure.

Skills created:
- 01-ourdigital-brand-guide: Brand reference & style guidelines
- 02-ourdigital-blog: Korean blog drafts (blog.ourdigital.org)
- 03-ourdigital-journal: English essays (journal.ourdigital.org)
- 04-ourdigital-research: Research prompts & workflows
- 05-ourdigital-document: Notion-to-presentation pipeline
- 06-ourdigital-designer: Visual/image prompt generation
- 07-ourdigital-ad-manager: Ad copywriting & keyword research
- 08-ourdigital-trainer: Training materials & workshop planning
- 09-ourdigital-backoffice: Quotes, proposals, cost analysis
- 10-ourdigital-skill-creator: Meta skill for creating new skills

Features:
- YAML frontmatter with "ourdigital" or "our" prefix triggers
- Standardized directory structure (code/, desktop/, shared/, docs/)
- Shared environment setup (_ourdigital-shared/)
- Comprehensive reference documentation
- Cross-skill integration support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 16:50:17 +07:00
parent 7d20abe811
commit 0bc24d00b9
169 changed files with 9970 additions and 741 deletions

View File

@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
Ghost CMS Publisher for OurDigital Blog
Publishes markdown content to Ghost CMS via Admin API.
Usage:
python ghost_publish.py --file post.md --draft
python ghost_publish.py --file post.md --publish
"""
import argparse
import os
import re
import jwt
import time
import requests
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables
load_dotenv(os.path.expanduser("~/.env.ourdigital"))
def create_ghost_token(admin_key: str) -> str:
"""Create JWT token for Ghost Admin API."""
key_id, secret = admin_key.split(":")
iat = int(time.time())
header = {"alg": "HS256", "typ": "JWT", "kid": key_id}
payload = {
"iat": iat,
"exp": iat + 5 * 60, # 5 minutes
"aud": "/admin/"
}
token = jwt.encode(payload, bytes.fromhex(secret), algorithm="HS256", headers=header)
return token
def parse_frontmatter(content: str) -> tuple[dict, str]:
"""Parse YAML frontmatter from markdown content."""
import yaml
pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)$'
match = re.match(pattern, content, re.DOTALL)
if match:
frontmatter = yaml.safe_load(match.group(1))
body = match.group(2)
return frontmatter, body
return {}, content
def publish_to_ghost(
file_path: str,
ghost_url: str,
admin_key: str,
draft: bool = True
) -> dict:
"""Publish markdown file to Ghost CMS."""
# Read and parse file
content = Path(file_path).read_text(encoding="utf-8")
frontmatter, body = parse_frontmatter(content)
# Create JWT token
token = create_ghost_token(admin_key)
# Prepare post data
post_data = {
"posts": [{
"title": frontmatter.get("title", "Untitled"),
"slug": frontmatter.get("slug"),
"html": markdown_to_html(body),
"meta_description": frontmatter.get("meta_description", ""),
"tags": [{"name": tag} for tag in frontmatter.get("tags", [])],
"status": "draft" if draft else "published",
"authors": [{"email": "andrew.yim@ourdigital.org"}]
}]
}
# Add feature image if provided
if frontmatter.get("featured_image"):
post_data["posts"][0]["feature_image"] = frontmatter["featured_image"]
# Make API request
api_url = f"{ghost_url}/ghost/api/admin/posts/"
headers = {
"Authorization": f"Ghost {token}",
"Content-Type": "application/json"
}
response = requests.post(api_url, json=post_data, headers=headers)
response.raise_for_status()
return response.json()
def markdown_to_html(markdown_text: str) -> str:
"""Convert markdown to HTML."""
try:
import markdown
return markdown.markdown(
markdown_text,
extensions=["tables", "fenced_code", "codehilite"]
)
except ImportError:
# Basic conversion if markdown library not available
html = markdown_text.replace("\n\n", "</p><p>")
html = f"<p>{html}</p>"
return html
def main():
parser = argparse.ArgumentParser(description="Publish to Ghost CMS")
parser.add_argument("--file", required=True, help="Markdown file to publish")
parser.add_argument("--draft", action="store_true", help="Publish as draft")
parser.add_argument("--publish", action="store_true", help="Publish immediately")
parser.add_argument("--channel", default="blog",
choices=["blog", "journal", "ourstory"],
help="Ghost channel to publish to")
args = parser.parse_args()
# Get credentials based on channel
channel_map = {
"blog": ("GHOST_BLOG_URL", "GHOST_BLOG_ADMIN_KEY"),
"journal": ("GHOST_JOURNAL_URL", "GHOST_JOURNAL_ADMIN_KEY"),
"ourstory": ("GHOST_OURSTORY_URL", "GHOST_OURSTORY_ADMIN_KEY")
}
url_var, key_var = channel_map[args.channel]
ghost_url = os.getenv(url_var)
admin_key = os.getenv(key_var)
if not ghost_url or not admin_key:
print(f"Error: Missing credentials for {args.channel}")
print(f"Set {url_var} and {key_var} in ~/.env.ourdigital")
return 1
try:
result = publish_to_ghost(
args.file,
ghost_url,
admin_key,
draft=not args.publish
)
post = result["posts"][0]
status = "Draft" if not args.publish else "Published"
print(f"{status}: {post['title']}")
print(f"URL: {ghost_url}/{post['slug']}/")
print(f"Edit: {ghost_url}/ghost/#/editor/post/{post['id']}")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
exit(main())