#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# KRIS CREATE — Content creator for krisyotam.com
#
# Maintainer: Kris Yotam <krisyotam@pm.me>
# License: MIT
# Created: 2026-02-15
# Description: Create new content entries. Collects title + preview from user,
# auto-generates globally unique slug, then delegates metadata
# decisions (category, tags, status, etc.) to Claude.
###############################################################################
###############################################################################
# config
###############################################################################
CONTENT_DB="/home/krisyotam/dev/krisyotam.com/public/data/content.db"
SYSTEM_DB="/home/krisyotam/dev/krisyotam.com/public/data/system.db"
GENERATE_SCRIPT="/home/krisyotam/dev/krisyotam.com/public/scripts/keep/generateMetadata.js"
CONTENT_DIR="$HOME/content"
ALL_TYPES=(
papers blog essays notes diary
progymnasmata reviews til now
)
# Content tables in content.db to check for slug collisions
CONTENT_TABLES=(
blog diary essays fiction news notes ocs
papers progymnasmata reviews verse
)
# System tables in system.db to check for slug collisions
SYSTEM_TABLES=(til now)
###############################################################################
# helpers
###############################################################################
have_cmd() { command -v "$1" >/dev/null 2>&1; }
die() { echo "ERROR: $1" >&2; exit 1; }
have_cmd fzf || die "fzf not found"
have_cmd sqlite3 || die "sqlite3 not found"
have_cmd claude || die "claude CLI not found"
have_cmd node || die "node not found"
[[ -f "$CONTENT_DB" ]] || die "content.db not found at $CONTENT_DB"
[[ -f "$GENERATE_SCRIPT" ]] || die "generateMetadata.js not found"
sql_content() { sqlite3 "$CONTENT_DB" "$1"; }
sql_system() { sqlite3 "$SYSTEM_DB" "$1" 2>/dev/null; }
###############################################################################
# ui
###############################################################################
print_banner() {
local cols
cols="$(tput cols 2>/dev/null || echo 80)"
if have_cmd figlet; then
figlet -w "$cols" -f big "CREATE" 2>/dev/null \
|| figlet -w "$cols" "CREATE"
else
echo "=== CREATE ==="
fi
echo
echo " New content entry for krisyotam.com"
echo " Metadata determined by Claude after analysis."
echo
}
###############################################################################
# slug generation + validation
###############################################################################
# Convert title to URL-safe slug
slugify() {
echo "$1" |
tr '[:upper:]' '[:lower:]' | # lowercase
sed 's/[^a-z0-9 -]//g' | # strip special chars
sed 's/ */ /g' | # collapse spaces
sed 's/ /-/g' | # spaces to hyphens
sed 's/--*/-/g' | # collapse hyphens
sed 's/^-//;s/-$//' # trim leading/trailing hyphens
}
# Check if slug exists in ANY content table (content.db) or system table (system.db)
# Returns: "type:title" if found, empty if unique
check_slug_globally() {
local slug="$1"
# Check all content.db tables
for table in "${CONTENT_TABLES[@]}"; do
local result
result="$(sql_content "SELECT '$table' || ':' || title FROM $table WHERE slug='$slug' LIMIT 1" 2>/dev/null)" || continue
if [[ -n "$result" ]]; then
echo "$result"
return
fi
done
# Check system.db tables
for table in "${SYSTEM_TABLES[@]}"; do
local result
result="$(sql_system "SELECT '$table' || ':' || title FROM $table WHERE slug='$slug' LIMIT 1" 2>/dev/null)" || continue
if [[ -n "$result" ]]; then
echo "$result"
return
fi
done
}
###############################################################################
# slug validation loop
###############################################################################
validate_slug() {
local slug_var="$1"
local slug="${!slug_var}"
while true; do
local collision
collision="$(check_slug_globally "$slug")"
if [[ -z "$collision" ]]; then
printf -v "$slug_var" '%s' "$slug"
echo " Slug: $slug"
return 0
fi
echo
echo " COLLISION: slug '$slug' already exists in: $collision"
echo " Enter alternative slug (or 'q' to cancel):"
read -rp " slug> " slug
[[ "$slug" == "q" ]] && return 1
[[ -z "$slug" ]] && return 1
# Re-slugify in case user typed something with spaces
slug="$(slugify "$slug")"
done
}
###############################################################################
# type-specific handlers
###############################################################################
handle_til() {
echo
echo " === Create TIL (Today I Learned) ==="
echo
read -rp " Title: " title
[[ -z "$title" ]] && { echo " Title required."; return 1; }
echo
echo " Enter content (type END on a new line when done):"
local content=""
while IFS= read -r line; do
[[ "$line" == "END" ]] && break
content+="$line"$'\n'
done
[[ -z "$content" ]] && { echo " Content required."; return 1; }
local slug
slug="$(slugify "$title")"
validate_slug slug || return 1
# TIL goes directly to generateMetadata.js, no Claude needed
echo
echo " Creating TIL entry..."
node "$GENERATE_SCRIPT" --type til --title "$title" --slug "$slug" --content "$content"
}
handle_now() {
echo
echo " === Create Now Update ==="
echo
echo " Enter content (type END on a new line when done):"
local content=""
while IFS= read -r line; do
[[ "$line" == "END" ]] && break
content+="$line"$'\n'
done
[[ -z "$content" ]] && { echo " Content required."; return 1; }
local slug
slug="$(date +%m-%Y)"
echo
echo " Creating Now entry..."
node "$GENERATE_SCRIPT" --type now --title "Now — $(date +%B\ %Y)" --slug "$slug" --content "$content"
}
handle_review() {
local title="$1" slug="$2" preview="$3"
# Reviews need a rating
local rating=""
while true; do
read -rp " Rating (1-10): " rating
if [[ "$rating" =~ ^[0-9]+$ ]] && (( rating >= 1 && rating <= 10 )); then
break
fi
echo " Invalid. Must be 1-10."
done
build_and_delegate "$slug" "reviews" "$title" "$preview" "--rating $rating"
}
handle_standard() {
local type="$1" title="$2" slug="$3" preview="$4"
build_and_delegate "$slug" "$type" "$title" "$preview" ""
}
###############################################################################
# claude delegation
###############################################################################
build_and_delegate() {
local slug="$1" type="$2" title="$3" preview="$4" extra_flags="$5"
local date
date="$(date +%Y-%m-%d)"
local script_path="node $GENERATE_SCRIPT"
local instruction="You are creating a new $type entry. Analyze the content and determine appropriate metadata following the taxonomy rules in CLAUDE.md.
=== USER INPUT ===
Type: $type
Title: $title
Preview: ${preview:-"(none)"}
Slug: $slug
Date: $date"
if [[ "$type" == "diary" ]]; then
instruction+="
=== YOUR TASK ===
1. ANALYZE the title and preview to understand the subject matter.
2. DETERMINE:
- Category: Choose from global categories
- Tags: Select 3+ relevant tags (MUST use existing tags only for diary entries)
3. CREATE the entry using this command:
$script_path --type diary --title \"$title\" --slug \"$slug\" --preview \"${preview:-""}\" --category <chosen-category> --tags \"<tag1,tag2,tag3>\""
elif [[ "$type" == "progymnasmata" ]]; then
instruction+="
=== YOUR TASK ===
1. ANALYZE the title and preview.
2. DETERMINE:
- Category: Choose from progymnasmata exercises (chreia, commonplace, comparison, confirmation, description, encomium, fable, impersonation, introduction-of-a-law, maxim, narrative, refutation, thesis, vituperation)
- Tags: Select 3+ relevant tags
- Status, Certainty, Importance
3. CREATE the entry using this command:
$script_path --type progymnasmata --title \"$title\" --slug \"$slug\" --preview \"${preview:-""}\" --category <chosen-category> --tags \"<tag1,tag2,tag3>\" --status <status> --certainty <certainty> --importance <1-10>"
elif [[ "$type" == "reviews" ]]; then
instruction+="
=== YOUR TASK ===
1. ANALYZE the title and preview.
2. DETERMINE:
- Category: Choose media type (anime, book, bookstores, film, manga, television)
- Tags: Select 3+ relevant tags
- Status, Certainty, Importance
3. CREATE the entry using this command:
$script_path --type reviews --title \"$title\" --slug \"$slug\" --preview \"${preview:-""}\" --category <chosen-category> --tags \"<tag1,tag2,tag3>\" --status <status> --certainty <certainty> --importance <1-10> $extra_flags"
else
instruction+="
=== YOUR TASK ===
1. ANALYZE the title and preview to understand the subject matter.
2. DETERMINE:
- Category: Choose from global categories (culture, film, history, literature, philosophy, psychology, science, technology, theology, on-myself, manuals-of-style, website)
- Tags: Select 3+ relevant tags following taxonomy rules
- Status: Assess completion (Notes, Draft, In Progress, Finished, Abandoned)
- Certainty: How verifiable (certain to impossible)
- Importance: Rate 1-10
3. CREATE the entry using this command:
$script_path --type $type --title \"$title\" --slug \"$slug\" --preview \"${preview:-""}\" --category <chosen-category> --tags \"<tag1,tag2,tag3>\" --status <status> --certainty <certainty> --importance <1-10>"
fi
instruction+="
4. VERIFY the entry was created successfully.
Execute this now without asking for confirmation. Make thoughtful metadata choices based on the content."
echo
echo " === Calling Claude Code ==="
echo " Claude will analyze your content and determine metadata..."
echo
claude -p "$instruction"
}
###############################################################################
# main
###############################################################################
main() {
clear
print_banner
# Select content type
local type
type="$(
printf "%s\n" "${ALL_TYPES[@]}" "quit" |
fzf --prompt="type> " --height=15 --reverse
)" || exit 0
[[ "$type" == "quit" ]] && exit 0
# Handle TIL and Now specially (they collect content, not just title/preview)
case "$type" in
til) handle_til; exit $? ;;
now) handle_now; exit $? ;;
esac
# Standard flow: title -> slug -> preview
echo
echo " === Create ${type^} Entry ==="
echo
read -rp " Title: " title
[[ -z "$title" ]] && { echo " Title required."; exit 1; }
local slug
slug="$(slugify "$title")"
# Validate slug globally
validate_slug slug || exit 1
read -rp " Preview/description: " preview
# Route to handler
case "$type" in
reviews) handle_review "$title" "$slug" "$preview" ;;
*) handle_standard "$type" "$title" "$slug" "$preview" ;;
esac
echo
echo " === Done ==="
}
main