#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" DEFAULT_BLOG_API_URL="https://blog.twisteddevices.com/api" if [[ -f "$PROJECT_ROOT/.env.local" ]]; then source "$PROJECT_ROOT/.env.local" fi export BLOG_API_URL="${BLOG_API_URL:-$DEFAULT_BLOG_API_URL}" if [[ -z "${BLOG_MACHINE_TOKEN:-}" ]]; then if [[ -n "${CRON_API_KEY:-}" ]]; then export BLOG_MACHINE_TOKEN="$CRON_API_KEY" fi fi source "${HOME}/.agents/skills/blog-backup/lib/blog.sh" url_encode() { jq -rn --arg value "${1:-}" '$value|@uri' } API_STATUS="" API_BODY="" api_call() { local method="$1" local endpoint="$2" local payload="${3:-}" local url="${BLOG_API_URL}${endpoint}" local curl_args=(-sS -X "$method" "$url") local tmp_body local http_code tmp_body="$(mktemp)" if [[ -n "${BLOG_MACHINE_TOKEN:-}" ]]; then curl_args+=(-H "x-api-key: ${BLOG_MACHINE_TOKEN}") fi if [[ -n "$payload" ]]; then curl_args+=(-H "Content-Type: application/json" -d "$payload") fi if ! http_code="$(curl "${curl_args[@]}" -o "$tmp_body" -w "%{http_code}")"; then rm -f "$tmp_body" return 1 fi API_STATUS="$http_code" API_BODY="$(cat "$tmp_body")" rm -f "$tmp_body" return 0 } is_json() { jq -e . >/dev/null 2>&1 <<<"${1:-}" } is_http_success() { local code="${1:-0}" [[ "$code" =~ ^[0-9]+$ ]] && (( code >= 200 && code < 300 )) } print_api_error() { local endpoint="$1" echo "❌ API request failed: ${BLOG_API_URL}${endpoint} (HTTP ${API_STATUS:-unknown})" >&2 if is_json "$API_BODY"; then local message message="$(jq -r '.error // .message // empty' <<<"$API_BODY")" if [[ -n "$message" ]]; then echo "Error: $message" >&2 fi echo "$API_BODY" | jq . >&2 return fi echo "Response (non-JSON):" >&2 printf '%s\n' "$API_BODY" | head -c 800 >&2 echo >&2 if [[ "${BLOG_API_URL}" == "https://blog.twisteddevices.com/api" && "$API_BODY" == *"This page could not be found."* ]]; then echo "Hint: /api/articles is not deployed there yet. For local testing use BLOG_API_URL=http://localhost:3002/api" >&2 fi } usage() { cat <<'EOF' Blog Backup CLI Usage: ./blog.sh [args] Environment: BLOG_API_URL API base (default: https://blog.twisteddevices.com/api) For local dev use: BLOG_API_URL=http://localhost:3002/api BLOG_MACHINE_TOKEN Optional machine token (falls back to CRON_API_KEY from .env.local) Modern article commands: article-add [args] Add one article (recommended) --title "..." Article title (required) --summary "..." Article summary (required) --url "https://..." Source URL (optional) --date YYYY-MM-DD Digest date (default: today) --published-at ISO8601 Published timestamp (optional) --tags "a,b,c" Comma-separated tags (optional) --json Output full JSON article-list [args] List article rows --date YYYY-MM-DD Filter by digest day --tag TAG Filter by tag --limit N Max rows (default: 50) --json Output full JSON article-delete Delete one article by id rss Print RSS feed URL Legacy digest commands: post [args] Create a bundled digest post (legacy) list [args] List digests (legacy) get Get digest by ID delete Delete digest by ID search Search digests by content status Check if digest exists for date health Check API health EOF } handle_article_add() { local TITLE="" local SUMMARY="" local SOURCE_URL="" local DIGEST_DATE="$(date +%F)" local PUBLISHED_AT="" local TAGS="" local JSON_OUTPUT=false while [[ $# -gt 0 ]]; do case "$1" in --title) TITLE="${2:-}"; shift 2 ;; --summary) SUMMARY="${2:-}"; shift 2 ;; --url) SOURCE_URL="${2:-}"; shift 2 ;; --date) DIGEST_DATE="${2:-}"; shift 2 ;; --published-at) PUBLISHED_AT="${2:-}"; shift 2 ;; --tags) TAGS="${2:-}"; shift 2 ;; --json) JSON_OUTPUT=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done if [[ -z "$TITLE" || -z "$SUMMARY" ]]; then echo "Usage: ./blog.sh article-add --title --summary <summary> [--url <url>] [--date <YYYY-MM-DD>] [--tags <a,b,c>]" >&2 exit 1 fi if [[ -z "${BLOG_MACHINE_TOKEN:-}" ]]; then echo "Error: BLOG_MACHINE_TOKEN (or CRON_API_KEY in .env.local) is required for article-add." >&2 exit 1 fi local payload payload="$(jq -n \ --arg title "$TITLE" \ --arg summary "$SUMMARY" \ --arg sourceUrl "$SOURCE_URL" \ --arg digestDate "$DIGEST_DATE" \ --arg publishedAt "$PUBLISHED_AT" \ --arg tagsCsv "$TAGS" \ '{ title: $title, summary: $summary, sourceUrl: (if $sourceUrl == "" then null else $sourceUrl end), digestDate: (if $digestDate == "" then null else $digestDate end), publishedAt: (if $publishedAt == "" then null else $publishedAt end), tags: (if $tagsCsv == "" then [] else ($tagsCsv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) end) }')" if ! api_call POST "/articles" "$payload"; then echo "❌ Failed to reach API endpoint." >&2 exit 1 fi if ! is_http_success "$API_STATUS"; then print_api_error "/articles" exit 1 fi if [[ "$JSON_OUTPUT" == "true" ]]; then if is_json "$API_BODY"; then echo "$API_BODY" | jq . else echo "$API_BODY" fi return fi if ! is_json "$API_BODY"; then echo "❌ API returned non-JSON success response." >&2 echo "$API_BODY" >&2 exit 1 fi echo "$API_BODY" | jq -r '"✅ Article added: \(.article.id // "unknown") | \(.article.title // "unknown")"' } handle_article_list() { local DATE="" local TAG="" local LIMIT=50 local JSON_OUTPUT=false while [[ $# -gt 0 ]]; do case "$1" in --date) DATE="${2:-}"; shift 2 ;; --tag) TAG="${2:-}"; shift 2 ;; --limit) LIMIT="${2:-}"; shift 2 ;; --json) JSON_OUTPUT=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done local endpoint="/articles?limit=${LIMIT}" if [[ -n "$DATE" ]]; then endpoint="${endpoint}&digestDate=$(url_encode "$DATE")" fi if [[ -n "$TAG" ]]; then endpoint="${endpoint}&tag=$(url_encode "$TAG")" fi if ! api_call GET "$endpoint"; then echo "❌ Failed to reach API endpoint." >&2 exit 1 fi if ! is_http_success "$API_STATUS"; then print_api_error "$endpoint" exit 1 fi if [[ "$JSON_OUTPUT" == "true" ]]; then if is_json "$API_BODY"; then echo "$API_BODY" | jq . else echo "$API_BODY" fi return fi if ! is_json "$API_BODY"; then echo "❌ API returned non-JSON response." >&2 echo "$API_BODY" >&2 exit 1 fi echo "$API_BODY" | jq -r '.articles[]? | "\(.id)\t\(.digestDate)\t\(.title)\t\(.sourceUrl // "-")"' } handle_article_delete() { local ID="${1:-}" if [[ -z "$ID" ]]; then echo "Usage: ./blog.sh article-delete <id>" >&2 exit 1 fi if [[ -z "${BLOG_MACHINE_TOKEN:-}" ]]; then echo "Error: BLOG_MACHINE_TOKEN (or CRON_API_KEY in .env.local) is required for article-delete." >&2 exit 1 fi local endpoint="/articles/${ID}" if ! api_call DELETE "$endpoint"; then echo "❌ Failed to reach API endpoint." >&2 exit 1 fi if ! is_http_success "$API_STATUS"; then print_api_error "$endpoint" exit 1 fi if is_json "$API_BODY"; then echo "$API_BODY" | jq . else echo "$API_BODY" fi } handle_rss() { local base_url="${BLOG_API_URL%/api}" echo "${base_url}/api/rss" } handle_post() { local DATE="" local CONTENT="" local TAGS='["daily-digest"]' while [[ $# -gt 0 ]]; do case "$1" in --date) DATE="$2"; shift 2 ;; --content) CONTENT="$2"; shift 2 ;; --tags) TAGS="$2"; shift 2 ;; *) echo "Unknown option: $1" >&2; shift ;; esac done if [[ -z "$DATE" ]]; then echo "Usage: ./blog.sh post --date <YYYY-MM-DD> --content <content> [--tags <tags>]" >&2 exit 1 fi if [[ -z "$CONTENT" ]]; then echo "Error: --content is required" >&2 exit 1 fi local DIGEST_ID DIGEST_ID=$(blog_post_create --date "$DATE" --content "$CONTENT" --tags "$TAGS") if [[ $? -eq 0 ]]; then echo "✅ Digest created: $DIGEST_ID" else echo "❌ Failed to create digest" >&2 exit 1 fi } handle_list() { local LIMIT=20 local SINCE="" local JSON_OUTPUT=false while [[ $# -gt 0 ]]; do case "$1" in --limit) LIMIT="$2"; shift 2 ;; --since) SINCE="$2"; shift 2 ;; --json) JSON_OUTPUT=true; shift ;; *) shift ;; esac done if [[ "$JSON_OUTPUT" == "true" ]]; then blog_post_list --limit "$LIMIT" ${SINCE:+--since "$SINCE"} --json | jq . else echo "=== Recent Digests ===" blog_post_list --limit "$LIMIT" ${SINCE:+--since "$SINCE"} fi } handle_get() { local ID="${1:-}" if [[ -z "$ID" ]]; then echo "Usage: ./blog.sh get <id>" >&2 exit 1 fi local RESULT RESULT=$(blog_post_get "$ID") if [[ -n "$RESULT" ]]; then echo "$RESULT" | jq . else echo "❌ Digest not found: $ID" >&2 exit 1 fi } handle_delete() { local ID="${1:-}" if [[ -z "$ID" ]]; then echo "Usage: ./blog.sh delete <id>" >&2 exit 1 fi blog_post_delete "$ID" } handle_search() { local QUERY="${1:-}" local LIMIT=20 if [[ -z "$QUERY" ]]; then echo "Usage: ./blog.sh search <query> [--limit N]" >&2 exit 1 fi if [[ "${2:-}" == "--limit" && -n "${3:-}" ]]; then LIMIT="$3" fi echo "=== Search Results for '$QUERY' ===" blog_post_search "$QUERY" --limit "$LIMIT" | jq -r '.[] | "\(.id) | \(.date) | \(.content | .[0:50])..."' } handle_status() { local DATE="${1:-}" if [[ -z "$DATE" ]]; then echo "Usage: ./blog.sh status <YYYY-MM-DD>" >&2 exit 1 fi if blog_post_status "$DATE"; then echo "✅ Digest exists for $DATE" else echo "❌ No digest found for $DATE" fi } handle_health() { echo "=== API Health Check ===" local RESULT RESULT=$(blog_health) if [[ -n "$RESULT" ]]; then echo "✅ API is accessible" echo "$RESULT" | jq . 2>/dev/null || echo "$RESULT" else echo "❌ API is not responding" >&2 exit 1 fi } main() { local cmd="${1:-}" shift || true case "$cmd" in article-add) handle_article_add "$@" ;; article-list) handle_article_list "$@" ;; article-delete) handle_article_delete "$@" ;; rss) handle_rss ;; post) handle_post "$@" ;; list) handle_list "$@" ;; get) handle_get "$@" ;; delete) handle_delete "$@" ;; search) handle_search "$@" ;; status) handle_status "$@" ;; health) handle_health ;; help|--help|-h|"") usage ;; *) echo "Unknown command: $cmd" >&2 usage exit 1 ;; esac } main "$@"