#!/bin/bash # mission-control-docs library - Full CRUD for Mission Control documents # Location: ~/.agents/skills/mission-control-docs/lib/docs.sh # Refactored to use API endpoints with machine token auth # Configuration MC_API_URL="${MC_API_URL:-https://mission-control-rho-pink.vercel.app/api}" MC_MACHINE_TOKEN="${MC_MACHINE_TOKEN:-}" # Error handler _error_exit() { echo "❌ $1" >&2 return 1 } # Make API call with machine token _api_call() { local method="$1" local endpoint="$2" local data="${3:-}" if [[ -z "$MC_MACHINE_TOKEN" ]]; then _error_exit "MC_MACHINE_TOKEN not set" return 1 fi local url="${MC_API_URL}${endpoint}" local curl_opts=( -s -H "Content-Type: application/json" -H "Authorization: Bearer ${MC_MACHINE_TOKEN}" ) if [[ -n "$data" ]]; then curl_opts+=(-d "$data") fi curl "${curl_opts[@]}" -X "$method" "$url" 2>/dev/null } # Detect folder based on content keywords (auto-categorization) _detect_folder() { local CONTENT="$1" local FOLDER="Research/" local LOWER_CONTENT=$(echo "$CONTENT" | tr '[:upper:]' '[:lower:]') if [[ "$LOWER_CONTENT" =~ (ai|agent|llm|claude|openclaw|machine learning|gpt|model) ]]; then FOLDER="Research/AI & Agents/" elif [[ "$LOWER_CONTENT" =~ (swift|ios|xcode|apple|app store|uikit|swiftui) ]]; then FOLDER="Research/iOS Development/" elif [[ "$LOWER_CONTENT" =~ (saas|business|startup|marketing|revenue|growth|sales) ]]; then FOLDER="Research/Business & Marketing/" elif [[ "$LOWER_CONTENT" =~ (tool|library|api|package|sdk|framework|npm|pip) ]]; then FOLDER="Research/Tools & Tech/" elif [[ "$LOWER_CONTENT" =~ (tutorial|guide|how.to|walkthrough|step.by.step) ]]; then FOLDER="Research/Tutorials/" fi echo "$FOLDER" } # Detect tags based on content keywords _detect_tags() { local CONTENT="$1" local TAGS=("saved" "article") local LOWER_CONTENT=$(echo "$CONTENT" | tr '[:upper:]' '[:lower:]') [[ "$LOWER_CONTENT" =~ (ai|agent|automation|llm|model) ]] && TAGS+=("ai" "agents") [[ "$LOWER_CONTENT" =~ openclaw ]] && TAGS+=("openclaw") [[ "$LOWER_CONTENT" =~ (swift|ios) ]] && TAGS+=("ios" "swift") [[ "$LOWER_CONTENT" =~ (business|saas) ]] && TAGS+=("business") [[ "$LOWER_CONTENT" =~ tutorial ]] && TAGS+=("tutorial") [[ "$LOWER_CONTENT" =~ (reference|documentation|docs) ]] && TAGS+=("reference") printf '%s\n' "${TAGS[@]}" | jq -R . | jq -s 'unique' } # Create document mc_doc_create() { local TITLE="" local CONTENT="" local FOLDER="" local TAGS="" local TYPE="markdown" local DESCRIPTION="" # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --title) TITLE="$2"; shift 2 ;; --content) CONTENT="$2"; shift 2 ;; --folder) FOLDER="$2"; shift 2 ;; --tags) TAGS="$2"; shift 2 ;; --type) TYPE="$2"; shift 2 ;; --description) DESCRIPTION="$2"; shift 2 ;; *) shift ;; esac done # Validate [[ -z "$TITLE" ]] && _error_exit "--title is required" && return 1 [[ -z "$CONTENT" ]] && _error_exit "--content is required" && return 1 # Clean content - remove control characters that break JSON local CLEAN_CONTENT=$(echo "$CONTENT" | tr -d '\000-\031' | sed 's/\\/\\\\/g') # Auto-detect folder/tags if not provided [[ -z "$FOLDER" ]] && FOLDER=$(_detect_folder "$CONTENT") [[ -z "$TAGS" ]] && TAGS=$(_detect_tags "$CONTENT") [[ -z "$DESCRIPTION" ]] && DESCRIPTION="Created: $(date +%Y-%m-%d)" # Build JSON payload local TAGS_JSON if [[ -n "$TAGS" ]]; then TAGS_JSON="$TAGS" else TAGS_JSON=$(_detect_tags "$CONTENT") fi local PAYLOAD=$(jq -n \ --arg title "$TITLE" \ --arg content "$CLEAN_CONTENT" \ --arg type "$TYPE" \ --arg folder "$FOLDER" \ --argjson tags "$TAGS_JSON" \ --arg description "$DESCRIPTION" \ '{ title: $title, content: $content, type: $type, folder: $folder, tags: $tags, description: $description }') # Create via API local RESPONSE=$(_api_call "POST" "/documents" "$PAYLOAD") local DOC_ID=$(echo "$RESPONSE" | jq -r '.id // .document?.id // empty') if [[ -n "$DOC_ID" && "$DOC_ID" != "null" ]]; then echo "$DOC_ID" return 0 fi _error_exit "Failed to create document: $(echo "$RESPONSE" | jq -r '.error // "Unknown error"')" return 1 } # Get document by ID mc_doc_get() { local DOC_ID="$1" local RESPONSE=$(_api_call "GET" "/documents?id=$DOC_ID") echo "$RESPONSE" | jq --arg id "$DOC_ID" '.documents | map(select(.id == $id)) | .[0] // empty' } # List documents with filters mc_doc_list() { local FOLDER="" local TAG="" local SEARCH="" local LIMIT=50 local JSON_OUTPUT=false # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --folder) FOLDER="$2"; shift 2 ;; --tags) TAG="$2"; shift 2 ;; --search) SEARCH="$2"; shift 2 ;; --limit) LIMIT="$2"; shift 2 ;; --json) JSON_OUTPUT=true; shift ;; *) shift ;; esac done # Get documents via API local RESPONSE=$(_api_call "GET" "/documents") local DOCUMENTS=$(echo "$RESPONSE" | jq '.documents // []') # Apply filters in jq if [[ -n "$FOLDER" ]]; then DOCUMENTS=$(echo "$DOCUMENTS" | jq --arg f "$FOLDER" 'map(select(.folder == $f))') fi if [[ -n "$TAG" ]]; then DOCUMENTS=$(echo "$DOCUMENTS" | jq --arg t "$TAG" 'map(select(.tags | index($t)))') fi if [[ -n "$SEARCH" ]]; then DOCUMENTS=$(echo "$DOCUMENTS" | jq --arg s "$SEARCH" 'map(select(.title | contains($s) or .content | contains($s)))') fi # Apply limit DOCUMENTS=$(echo "$DOCUMENTS" | jq --argjson limit "$LIMIT" '.[:$limit]') if [[ "$JSON_OUTPUT" == true ]]; then echo "{\"documents\": $DOCUMENTS}" else echo "$DOCUMENTS" | jq -r '.[] | "\(.folder) | \(.title) | \(.id)"' 2>/dev/null fi } # Update document mc_doc_update() { local DOC_ID="$1" shift # First get the current document local CURRENT_DOC=$(mc_doc_get "$DOC_ID") [[ -z "$CURRENT_DOC" ]] && _error_exit "Document not found: $DOC_ID" && return 1 # Build updates local UPDATES="{}" while [[ $# -gt 0 ]]; do case $1 in --title) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {title: $v}'); shift 2 ;; --content) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {content: $v}'); shift 2 ;; --folder) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {folder: $v}'); shift 2 ;; --tags) UPDATES=$(echo "$UPDATES" | jq --argjson v "$2" '. + {tags: $v}'); shift 2 ;; --description) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {description: $v}'); shift 2 ;; *) shift ;; esac done # Merge current document with updates local MERGED_DOC=$(echo "$CURRENT_DOC" | jq --argjson updates "$UPDATES" '. + $updates') # Send update via API local PAYLOAD=$(jq -n --arg id "$DOC_ID" --argjson doc "$MERGED_DOC" '{id: $id, document: $doc}') local RESPONSE=$(_api_call "PATCH" "/documents" "$PAYLOAD") if echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then _error_exit "Update failed: $(echo "$RESPONSE" | jq -r '.error')" return 1 fi echo "✅ Document updated" } # Delete document mc_doc_delete() { local DOC_ID="$1" _api_call "DELETE" "/documents" "{\"id\": \"$DOC_ID\"}" >/dev/null echo "✅ Document deleted" } # Search documents mc_doc_search() { local QUERY="$1" local LIMIT="${2:-20}" # Get documents and filter client-side local RESPONSE=$(_api_call "GET" "/documents") echo "$RESPONSE" | jq --arg q "$QUERY" --argjson limit "$LIMIT" ' .documents | map(select(.title | test($q; "i") or .content | test($q; "i"))) | .[:$limit] ' } # List folders mc_doc_folder_list() { # Get all documents and extract unique folders local RESPONSE=$(_api_call "GET" "/documents") echo "$RESPONSE" | jq -r '.documents[].folder' | sort -u } # Export functions export -f _detect_folder export -f _detect_tags export -f _api_call export -f mc_doc_create export -f mc_doc_get export -f mc_doc_list export -f mc_doc_update export -f mc_doc_delete export -f mc_doc_search export -f mc_doc_folder_list