- gantt-tasks: Replace Supabase REST with API calls using GANTT_MACHINE_TOKEN - mission-control-docs: Replace Supabase REST with API calls using MC_MACHINE_TOKEN - Both skills now follow API-centric architecture - Updated SKILL.md documentation for both This ensures consistency with the CLI auth pattern and provides single source of truth through API endpoints.
288 lines
8.6 KiB
Bash
288 lines
8.6 KiB
Bash
#!/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
|